java内存分析(3)——java的常量池的理解

java执行类的阶段划分

jvm在执行某个类的时候,必须经过加载连接初始化,而连接又包括验证准备解析三个阶段。

image-20211023174945692

常量池分类

常量池可分为三类:class文件常量池、运行时常量池、全局字符串池

  1. class文件常量池(class constant pool):
  • 出现时间:在类加载之前就存在于class文件中了
  • 存放内容:对象的符号引用值
  • 存放在class文件中
  • class常量池中存放的是字面量和符号引用,也就是说它们存的不是对象的实例,而是对象的符号引用值
  1. 运行时常量池(runtime constant pool):通常所说的常量池,指的就是运行时常量池
  • 出现时间:在类加载之后
  • 存放内容:对象的符号引用值,经过解析阶段后,替换为直接引用
  • 在class文件被加载进了内存之后,常量池保存在了方法区中,所以,每一个类都有一个运行时常量池
  • 在class文件加载之后,jvm就会将class常量池中的符号引用解析后,替换为直接引用存放到运行时常量池中(也就是符号引用)
  1. 全局字符串池(String pool或者String literal poll):
  • 出现时间:在准备阶段之后(因为解析阶段运行时常量池需要查询全局字符串)
  • 存放内容:驻留字符串的引用
  • 在类加载完成,经过验证,准备阶段之后,在堆中生成字符串对象实例之后,再将该字符串对象实例的引用值存到string pool中。
  • string pool的功能由谁实现?由StringTable类实现,它是一个哈希表,里面存的是驻留字符串的引用,可以依据它找到堆中的字符串实例
  • 这个StringTable在每个htoSpot VM 的实例只有一份,也就是说它被所有的类共享

image-20211023224256460

案例

1
2
3
4
5
6
7
8
9
String str1 = "abc";
String str2 = new String("def");
String str3 = "abc";
String str4 = str2.intern();
String str5 = "def";

System.out.println(str1 == str3);//true
System.out.println(str2 == str4);//false
System.out.println(str4 == str5);//true

上面的例子虽然简单,但是,几种情况都已经囊括在内了

分析第一句

1
String str1 = "abc";

这一句,在类加载完成后,首先,会在堆中生成一个“abc”实例;然后全局StringTable中存放着“abc”的引用值

分析第二句(注意点)

1
String str2 = new String("def");

这一句,在类加载完成后,首先,会在堆中生成一个“def”实例;然后全局StringTable中存放着“def”的引用值;接着,new 出来一个”def“的实例(这两个实例不是同一个);

生成了两个对象实例!!!

生成了两个对象实例!!!

生成了两个对象实例!!!

image-20211023183033844

反编译后可以看到,该语句产生了两个实例

重点:new出来的字符串实例,和在全局字符串值中对应的那个字符串实例,是不同的实例

分析第三句

1
String str3 = "abc";

这一句,在类加载完成后,进行解析,会在StringTable里面找到”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同

分析第四句(关键是intern函数)

1
String str4 = str2.intern();

这一句,在类加载完成后,str4是在运行的时候调用**intern()函数**,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值

image-20211023183338749

分析反编译后的代码,可以看到,该语句并没有生成新的字符串对象

分析第五句

1
String str5 = "def";

这一句,在类加载完成后,在这里,由于StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值

总结

java的常量池主要有这么几个内容:

  1. class常量池:①在编译后就有 ②存储的是对象的符号引用
  2. 全局字符串池:①在准备阶段后、解析阶段前出现 ②存储的是驻留字符串的引用 ③运行时常量池在解析阶段,要进行查询,确保两个常量池的引用时一致的
  3. 运行时常量池:显示class常量池中的引用,解析阶段后替换为对象的直接引用

应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
String c = "a" + "b";
String d = a + "b";
String e = a + b;
String a2 = new String("a");
String ab2 = a2 + b;
String ab3 = a2 + "b";
System.out.println(a == a2); //false
System.out.println(ab == c); //true
System.out.println(c == d); //false
System.out.println(c == e); //false
System.out.println(d == e); //false
System.out.println(ab2 == ab); //false
}

在上述例子中,得出一下结论:

①直接用引号的(可以有+号进行连接),内容也一样的,引用的就是一个对象

②如果出现类似String c = b + "a";这样的,那么就是在堆中生成了一个新的对象。

补充

今天偶然间看到了韩顺平老师在B站讲String的特性的课程,看完后,忽然间觉得自己这块瞬间好像明朗了

(上面的内容虽然也差不多,但总觉得还是繁琐,好像有哪些地方不透彻)

主要是补充对于下面这一句代码的理解(根据韩顺平老师将它底层分析的理解):

1
2
3
4
String a = "a";
String b = "b";
String ab = a+b; //这句是重点,在这一句的背后,底层做了很多事情
String c = "ab";

不知道为什么韩老师的在调试的时候能够step into进去查看底层执行情况,而我的就不行img

但是不影响理解它

显然执行过下面两句后,在字符串常量池中,分别存储了两个字符串:“a”,“b”

1
2
String a = "a";
String b = "b";

然后正片开始!

接下来执行:String ab = a + b;

其实,这句在底层做了很多事情。

  1. 先创建了一个空的StringBuilder的对象,这个对象是存储在堆中的(先暂时存储一下字符串数据,主要要使用它的append方法)
  2. 然后先向其中追加了“a”(使用的是它的append方法)
  3. 然后又向其中追加了“b”(使用的还是它的append方法)
  4. 然后执行了(且返回给ab)return new String(value,0,count) (value指的是上面的StringBuilder对象),这样在字符串常量池中就有了“ab”了,同时堆中也new了一个String类型的对象(值就是value中的数据),并且ab就是指向了这个String类型的对象(不是字符串常量池中的)——(这里和是上面的分析第二句是一样的原理)
Contents
  1. 1. java执行类的阶段划分
  2. 2. 常量池分类
  3. 3. 案例
    1. 3.1. 分析第一句
    2. 3.2. 分析第二句(注意点)
    3. 3.3. 分析第三句
    4. 3.4. 分析第四句(关键是intern函数)
    5. 3.5. 分析第五句
  4. 4. 总结
  5. 5. 应用
  6. 6. 补充
|