如果你点开了本篇文章,那么恭喜你发现宝藏了!
博主接下来将会更新整个系列的 《探索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类型的解析工作。
如果当前字符串内容存在于字符串常量池中,即使用 equas() 方法返回ture,那直接返回此字符串在常量池的引用。如果不在字符串常量池中,那么在常量池创建一个引用并且指向堆中已存在的字符串,然后返回常量池中的地址。是不是有点抽象,对着面试题再看一遍。
注意:该方法是有返回值的,返回的是常量池中的地址。为什么要强调呢?看面试题。
==比较的是引用,即内存地址。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: true3、解析
【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底层知识,请一定要点赞+关注,一键三连!