探索JVM的底层秘密(一)——如何正确理解Java的常量池

断了今生、忘了曾经 提交于 2020-10-23 15:52:38

如果你点开了本篇文章,那么恭喜你发现宝藏了!

博主接下来将会更新整个系列的 《探索JVM的底层秘密》 文章,为大家完整的剖析JVM的底层原理。

作者最近在优化JVM内存模型这方面的内容,发现自己对于Java中的常量池的理解有点零碎,做个总结,于是就有了这篇文章。本篇文章所有知识点基于jdk8。
jdk6、jdk7不适用,如果有疑问,欢迎在评论区留言。废话不多说,直接上代码。

比如你写了一段这样的Java代码,JVM是如何处理的呢?


1、Java代码

public class StringTest2 {
   
   
    String name = "子牙";
    
    public static void main(String[] args) {
   
   
        StringTest2 obj = new StringTest2();
    }
}

2、class文件之常量池(图1)

在这里插入图片描述


3、默认构造方法字节码(图2)

在这里插入图片描述




常量池分类

1、class文件中的常量池

这个常量池中主要存放两大类常量:字面量、符号引用。

字面量即文本字符串,如index=10的Code、index=11的LineNumberTable……还有声明为final的常量。

符号引用则属于编译原理方面的概念,包含三类:

  • 类和接口的全限定名,如index=4存放的是CONSTANT_Class_info结构,指向的是类的全限定名
  • 字段的名称和描述符,如index=3存放的是CONSTANT_Fieldref_info结构,指向的是字段的名称与描述符
  • 方法的名称和描述符,如index=17存放的是CONSTANT_Methodref_info结构,指向的是方法的名称与描述符

2、运行时常量池

方法区的一部分。我们常说的常量池,就是指这一块区域:方法区中的运行时常量池。

那数据是何时存入这块区域的呢?是在类加载阶段,类加载器子系统会将class文件中的常量池中的数据封装成相应的CONSTANT_*结构存入进去。

这里重点说下index=2的数据项。index=2对应的数据结构是CONSTANT_String,但是在类加载阶段,index=2存储的数据结构却是JVM_CONSTANT_UnresolvedString,为什么会这样呢?因为加载类的时候,还没有解析字符串字面量,即没有将符号引用转为直接引用。那何时解析的呢?执行引擎执行ldc指令的时候。不懂?往后看。

3、全局字符串常量池

这个常量池在JVM层面就是一个StringTable,只存储对java.lang.String实例的引用,而不存储String对象的内容。

一般我们说一个字符串进入了字符串常量池其实是说在这个StringTable中保存了对它的引用,反之,如果说没有在其中就是说StringTable中没有对它的引用。

解析上面的代码

基础知识讲完了,咱们来实战一下。就以JVM处理上面贴出的代码为例,给童鞋们分享一下执行流程:

1、调用javac命令编译java文件生成class文件,class文件中的常量池(图1)

2、调用java命令开始运行这个class文件,类加载器子系统将class文件加载进内存,并将常量池中的数据封装成相应的CONSTANT_*结构存入运行时常量池。这时候常量池中index=2的位置存放的是JVM_CONSTANT_UnresolvedString而不是JVM_CONSTANT_String_info

3、执行引擎运行StringTest2的默认构造函数,即图3。大家是否注意到ldc指令,那这个指令做了什么呢?执行引擎执行ldc指令时,会根据ldc后面的操作数去运行时常量池中查找对应的值,并判断是否已完成解析,如果已解析就直接返回字符串在堆中的引用,即内存地址。如果没有解析进去解析,那如何解析呢?

4、根据JVM_CONSTANT_UnresolvedString中存放的index去运行时常量池中查找CONSTANT_Utf8_info结构,这个结构存放了字符串的具体内容及字符串长度。然后判断字符串常量池中是否有这个字符串的引用,如果有就直接返回,如果没有就去堆中创建一个对应内容的String对象,并将引用存储在字符串常量池中。这样就完成了String类型的解析工作。

intern方法做了什么

如果当前字符串内容存在于字符串常量池中,即使用 equas() 方法返回ture,那直接返回此字符串在常量池的引用。如果不在字符串常量池中,那么在常量池创建一个引用并且指向堆中已存在的字符串,然后返回常量池中的地址。是不是有点抽象,对着面试题再看一遍。

注意:该方法是有返回值的,返回的是常量池中的地址。为什么要强调呢?看面试题。

==与equals

==比较的是引用,即内存地址。equals比较的是两个对象的内容。

字符串相关面试题剖析

如果你对Java中的常量池理解得不是很透彻,这道面试题你还真不一定能答上来。就算告诉了你答案你可能也会一脸懵逼。那这篇文章你已经看到这里了,我希望你已明了。建议同学们先不要看答案以及我的解析,先自己回答一下,然后给出自己的分析,再看答案。

1、上代码


public class StringTest1 {
   
    
    public static void main(String[] args) {
   
    
        String s1 = "子牙真帅";
        String s2 = "子牙真帅";

        String a = "子牙";

        String s3 = new String(a + "真帅");
        String s4 = new String(a + "真帅");

        System.out.println("s1 == s2: " + (s1 == s2));
        System.out.println("s2 == s3: " + (s2 == s3));
        System.out.println("s3 == s4: " + (s3 == s4));

        s3.intern();
        System.out.println("s2 == s3: " + (s2 == s3));

        s3 = s3.intern();
        System.out.println("s2 == s3: " + (s2 == s3));
    }
}

2、返回结果

s1 == s2: true
s2 == s3: false
s3 == s4: false
s2 == s3: false
s2 == s3: true

3、解析

  • 【s1 == s2: true】:因为s1、s2都指向字符串常量池中同一字符串:hello
  • 【s2 == s3: false】:因为s2是指向字符串常量池中的引用,s3是指向堆中的引用,自然不相等
  • 【s3 == s4: false】:因为s3、s4是两个不同的对象,自然不相等
  • 【s2 == s3: false】:因为s3虽然调用了intern方法,但是未处理返回值,所以s3依然是指向堆中的引用
  • 【s2 == s3: true】:因为s3调用了intern方法,并且返回给了s3,此时 s2、s3 都直接指向常量池的同一个字符串。

好了,今天的文章就暂时先写到这里了,如果本篇文章对你有帮助,想要继续了解之后的更多JVM底层知识,请一定要点赞+关注,一键三连!


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!