Class文件常量池
Class文件中除了有类的版本、方法、字段等描述信息以外,还有一项信息是常量池,用于存放编译期生成的字面量和符号引用。这部分内容将在类加载以后存放在运行时常量池中。
字面量
字面量包括字符串和被final修饰的变量。下面几张图是Class常量池里的字面量(通过jclasslib查看,也可以通过javap -v XXX.class查看字节码信息)
符号引用
符号引用包括以下:
类和接口的全限定名称,比如StringBuilder类的全限定名就是:java/lang/StringBuilder
字段的名称和描述符:名称即代码中定义的变量名(包括成员变量、实例变量(实例变量如果是基本数据类型,那么只保存名称和描述符,不保存字面量)和局部变量(描述同实例变量)),描述符(比如: int 变量描述符为I,int[] 为[I)
- 方法的名称和描述符:名称即代码中定义的方法名,描述符:按照先参数列表后返回值的顺序描述(比如:String::toString()的描述符为 ()Ljava/lang/String);
运行时常量池
运行时常量池是方法区(JDK1.7之前用永久代来实现方法区,JDK1.8之后改为元空间)的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池(Constant Pool Table),存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。
我们上面所说的class文件常量池在类加载后会进入方法区中的运行时常量池中,并且需要注意的是运行时常量池是全局共享的,多个类共享一个运行时常量池。
值得一提的是,运行时常量池相对于class文件常量池的另一个特征就是具有动态性,Java语言并不要求常量只在编译时期产生。运行期也可能有新的常量放入常量池中(否则为什么叫运行时常量池呢,如果所有的常量都在编译时产生那么运行时常量池和class文件常量池又有什么区别)。典型的应用就是String::intern()。
在JDK1.6的时候,调用这个方法虚拟机会在字符串常量池在查找是否有与当前字符串相等(equals)的对象,如果有,则返回这个对象;如果没有,则会在字符串常量池中添加这个对象。注意,是把这个对象添加到字符串常量池。
在jdk1.7以后,调用这个方法虚拟机会在字符串常量池在查找是否有与当前字符串相等(equals)的对象,如果有,则返回这个对象的引用;如果没有,则会在字符串常量池中添加这个对象的引用。注意,这个时候添加的是对象在堆中的引用。
字符串常量池(String constant pool)
字符串常量池是用来存放字符串的也就是说class文件常量池中的文本字符串会在类加载时进入字符串常量池。那么字符串常量池和运行时常量池是什么关系呢?即运行时常量池逻辑上是包含字符串常量池的。然而,在jdk1.7以后,字符串常量池就被移出了方法区,放到了堆内存中,此时运行时常量池剩下的部分还在永久代,运行时常量池逻辑上是包含字符串常量池的。jdk1.8以后,永久代也被移除了,jvm使用元空间(Meta space)代替永久代成为方法区的具体实现。
HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet
一般我们说一个字符串进入了全局的字符串常量池其实是说在这个StringTable中保存了对它的引用,反之,如果说没有在其中就是说StringTable中没有对它的引用。
在JDK6.0 中,StringTable 的长度是固定的,长度就是1009,因此如果放入 String Pool 中的 String 非常多,就会造成 hash 冲突,导致链表过长,当调用 String#intern() 时会需要到链表上一个一个找,从而导致性能大幅度下降;
在 JDK7.0以上版本中,StringTable 的长度可以通过参数指定:
1 | -XX:StringTableSize=66666 |