心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池

2024-06-03 08:58

本文主要是介绍心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

追逐仰望星空 2020-07-09  

推荐学习

  1. 肝了十天半月,献上纯手绘“Spring/Cloud/Boot/MVC”全家桶脑图
  2. “走后门”成功!万分感激腾讯大佬的手敲“高分JVM”秘籍
  3. 最新“美团+字节+腾讯”一二三面问题,挑战一下你能走到哪一面?

本篇博客由于比较深入的写进JVM底层,所以如果有错误希望可以指出咱们共同讨论

目录

1.常量池与Class常量池

2.运行时常量池

  • 运行时常量池的简介
  • 方法区的Class文件信息, Class常量池和运行时常量池的三者关系

3.字符串常量池

  • 字符串常量池的简介
  • 采用字面值的方式创建字符串对象
  • 采用new关键字新建一个字符串对象
  • 字符串池的优缺点

4.字符串常量池和运行时常量池之间的藕断丝连

  • 常量池和字符串常量池的版本变化
  • String.intern在JDK6和JDK7之后的区别(重难点)

心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池

 

1.常量池

常量池,也叫 Class 常量池(常量池==Class常量池)。Java文件被编译成 Class文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池,常量池是当Class文件被Java虚拟机加载进来后存放在方法区 各种字面量 (Literal)和 符号引用 。

在Class文件结构中,最头的4个字节用于 存储魔数 (Magic Number),用于确定一个文件是否能被JVM接受,再接着4个字节用于 存储版本号,前2个字节存储次版本号,后2个存储主版本号,再接着是用于存放常量的常量池。常量池主要用于存放两大类常量:字面量和符号引用量,字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念。如下

心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池

 

心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池

 

2.运行时常量池

2.1运行时常量池的简介

运行时常量池是方法区的一部分。运行时常量池是当Class文件被加载到内存后,Java虚拟机会 将Class文件常量池里的内容转移到运行时常量池里(运行时常量池也是每个类都有一个)。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中

2.2方法区的Class文件信息,Class常量池和运行时常量池的三者关系

心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池

 

字符串常量池

3.1字符串常量池的简介

字符串常量池又称为:字符串池,全局字符串池,英文也叫String Pool。 在工作中,String类是我们使用频率非常高的一种对象类型。JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间,这就是我们今天要讨论的核心:字符串常量池。字符串常量池由String类私有的维护。

我们理清几个概念:

在JDK7之前字符串常量池是在永久代里边的,但是在JDK7之后,把字符串常量池分进了堆里边。看下面两张图:

心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池

 

在堆中的字符串常量池: 里边的字符串常量池存放的是字符串的引用或者字符串(两者都有)下面例子会有具体的讲

心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池

 

符号引用表会在下面讲

我们知道,在Java中有两种创建字符串对象的方式:

  1. 采用字面值的方式赋值
  2. 采用new关键字新建一个字符串对象。这两种方式在性能和内存占用方面存在着差别。

3.2采用字面值的方式创建字符串对象

package Oneday;
public class a {public static void main(String[] args) {String str1="aaa";String str2="aaa";System.out.println(str1==str2);   }
}
运行结果:
true

采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在"aaa"这个对象,如果不存在,则在字符串池中创建"aaa"这个对象,然后将池中"aaa"这个对象的引用地址返回给字符串常量str,这样str会指向池中"aaa"这个字符串对象;如果存在,则不创建任何对象,直接将池中"aaa"这个对象的地址返回,赋给字符串常量。

对于上述的例子:这是因为,创建字符串对象str2时,字符串池中已经存在"aaa"这个对象,直接把对象"aaa"的引用地址返回给str2,这样str2指向了池中"aaa"这个对象,也就是说str1和str2指向了同一个对象,因此语句System.out.println(str1== str2)输出:true

3.3采用new关键字新建一个字符串对象

package Oneday;
public class a {public static void main(String[] args) {String str1=new String("aaa");String str2=new String("aaa");System.out.println(str1==str2);}
}
运行结果:
false

采用new关键字新建一个字符串对象时,JVM首先在字符串常量池中查找有没有"aaa"这个字符串对象。如果有,则不在池中再去创建"aaa"这个对象,直接在堆中创建一个"aaa"字符串对象,然后将堆中的这个"aaa"对象的地址返回赋给引用str1,这样,str1就指向了堆中创建的这个"aaa"字符串对象;如果没有,则首先在字符串常量池池中创建一个"aaa"字符串对象,然后再在堆中创建一个"aaa"字符串对象,然后将堆中这个"aaa"字符串对象的地址返回赋给str1引用,这样,str1指向了堆中创建的这个"aaa"字符串对象。

对于上述的例子:

因为,采用new关键字创建对象时,每次new出来的都是一个新的对象,也即是说引用str1和str2指向的是两个不同的对象,因此语句

System.out.println(str1 == str2)输出:false

字符串池的实现有一个前提条件:String对象是不可变的。因为这样可以保证多个引用可以同时指向字符串池中的同一个对象。如果字符串是可变的,那么一个引用操作改变了对象的值,对其他引用会有影响,这样显然是不合理的。

3.4字符串池的优缺点

字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;

另一方面,字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低

4.字符串常量池和运行时常量池之间的藕断丝连

博主为啥要把他俩放在一起讲呢,主要是随着JDK的改朝换代,字符串常量池有很大的变动,和运行时常量池有关。而且网上众说纷纭,我真的在看的时候ctm了,所以博主花很长时间把这一块讲明白,如果有错误或者异议可以通知博主。博主一定会在第一时间参与讨论

4.1常量池和字符串常量池的版本变化

  • 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
  • 在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说 字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
  • 在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

4.2String.intern在JDK6和JDK7之后的区别(重点)

JDK6和JDK7中该方法的功能是一致的,不同的是常量池位置的改变(JDK7将常量池放在了堆空间中),下面会具体说明。intern的方法返回字符串对象的规范表示形式。其中它做的事情是:首先去判断该字符串是否在常量池中存在,如果存在返回常量池中的字符串,如果在字符串常量池中不存在,先在字符串常量池中添加该字符串,然后返回引用地址

例子1:

String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);运行结果:
JDK6运行结果:false
JDK7运行结果:false

我们首先看一张图:

心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池

 

上边例子中s1是new出来对象存放的位置的引用,s2是存放在字符串常量池的字符串的引用,所以两者不同

例子2:

String s1 = new String("1");
System.out.println(s1.intern() == s1);运行结果:
JDK6运行结果:false
JDK7运行结果:false

上边例子中s1是new出来对象存放的位置的引用,s1.intern()返回的是字符串常量池里字符串的引用

例子3:

String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1 == s2);
运行结果:
JDK6运行结果:false
JDK7运行结果:true

JDK6中,s1.intern()运行时,首先去常量池查找,发现没有该常量,则在常量池中开辟空间存储"11",返回常量池中的值(注意这里也没有使用该返回值),第三行中,s2直接指向常量池里边的字符串,所以s1和s2不相等。有可能会有小伙伴问为啥s1.intern()发现没有该常量呢,那是因为:

String s1 = new String(“1”) + new String(“1”);这行代码实际操作是,创建了一个StringBuilder对象,然后一路append,最后toString,而toString其实是又重新new了一个String对象,然后把对象给s1,此时并没有在字符串常量池中添加常量

JDK7中,由于字符串常量池在堆空间中,所以在s1.intern()运行时,发现字符串常量池没有常量,则添加常量并使其指向堆空间地址,返回堆空间地址(注意这里也没有使用该返回值),这时s2通过查找常量池中的常量,同样也指向了堆空间地址,所以s1和s2相等。

例子4:

String s1 = new String("1") + new String("1");
System.out.println(s1.intern() == s1);

JDK6中,常量池在永久代中,s1.intern()去常量池中查找"11",发现没有该常量,则在常量池中开辟空间存储"11",返回常量池中的值,s1指向堆空间地址,所以二者不相等。

JDK7中,常量池在堆空间,s1.intern()去常量池中查找"11",发现没有该常量,则在字符串常量池中开辟空间,指向堆空间地址,则返回字符串常量池指向的堆空间地址,s1也是堆空间地址,所以二者相等。

例子5:

package Oneday;
public class a {public static void main(String[] args) {String str=new String("123");System.out.println(str.intern()==str);}
}
运行结果:
JDK7:false

因为String str=new String("123");表示在字符串常量池和堆空间都有“123”这个常量

作者:远赴人间,一睹世颜

原文链接:https://blog.csdn.net/qq_45737068/article/details/107149922

这篇关于心血总结之JVM:详解JVM常量池、Class、运行时、字符串常量池的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1026531

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p