解析字符串常量池,运行时常量池之间的关系,及intern方法
学习JVM过程中,对于这部分内容有些疑惑,查阅资料时,发现说法各不相同,有些博文甚至自相矛盾,通过《深入理解Java虚拟机》和几篇觉得有借鉴意义的博文,加上一些代码实测,整理出以下在我看来逻辑通顺的内容,错误之处希望能够得到指正。
参考文章:
Java中的常量池(字符串常量池、class常量池和运行时常量池)
1.字符串常量池(String Constant Pool):
-
首先是字符串常量池部分,这里要明确一点的是,在 JDK1.6 以及以前的版本中,字符串的常量池是放在heap的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 时会直接产生异常:
----java.lang.OutOfMemoryError: PermGen space
--------at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(类名.java:行数)
可见JDK1.6及之前字符串常量池在运行时常量池中。 -
在JDK1.7的版本中,字符串常量池已经从Perm区移到Java Heap区域了。为什么要移动,Perm区域太小是一个主要原因, PermGen区在JDK1.8及以后版本被移除,使用Metaspace(元空间)来实现JVM规范中的方法区 ,元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
字符串常量池存放区域
- 在JDK1.6及之前版本,字符串常量池是放在PermGen区(1.8以前方法区的实现)中;
- 在JDK1.7及以后版本,字符串常量池被移到了heap中了;
字符串常量池实现原理
- 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009,不能自动扩容;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
- 在JDK1.6中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
- 在JDK1.7中,StringTable的长度可以通过参数指定:-XX:StringTableSize=9999
字符串常量池存放内容
- 在JDK1.6及之前版本中,String Pool里放的都是字符串常量;
- 在JDK1.7及以后版本中,由于String#intern()方法发生了改变(后续intern内容中会详细讨论),因此String Pool中也可以存放heap内的字符串对象的引用。
应该注意的是:无论是字符串常量池中的字符串常量,还是引用的字符串对象,相同内容的字符串只在字符串常量池中存在一份,可见如下代码,其中intern部分后续分析:
public class StringPoolTest {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
String s3 = new String("de")+new String("f");
s3.intern();
String s4 = "def";
String s5 = new String("ghi");
s5.intern();
String s6 = "ghi";
System.out.println(s1 == s2); //结果为true
System.out.println(s3 == s4); //结果为true
System.out.println(s5 == s6); //结果为false
/*结果为1.7及以后版本结果,1.6会有所差异*/
}
}
分析以上代码:
- s1==s2 true,String s1 = “abc” 会在字符串常量池中生成"abc"," "之中的字符串在执行到当前语句时会在字符串常量池中构建一个实例,s1,s2都会指向字符串常量池中的"abc"的地址;
- s3==s4 true,在后续intern内容中详细说明,对于JDK1.7及之后结果为true,而之前的版本结果为false;
- s5==s6 false,同上在后续intern内容中详细说明,1.6及1.7以后版本结果均为false;
2.class常量池(Class Constant Pool):
class常量池简介:
- 我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
- 每个class文件都有一个class常量池。
什么是字面量和符号引用:
- 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
- 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
3.运行时常量池(Runtime Constant Pool):
- 运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,但是相对于class常量池,其具备动态性,不要求常量一定自由编译期才能产生,也就是并非只有预置入class文件中的常量池内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中;
- JDK1.7及之后版本字符串常量池不再位于运行时常量池中,如开篇所述位于Java Heap区;
- JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
4.intern()方法详解
该部分内容与开篇提到的美团技术团队深入解析String#intern基本相同,但是上述文章中存在错误之处,在于对JDK1.6版本执行代码结果的分析。
intern()方法实现原理
-
JDK1.7及以后intern()的源码
/** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */ public native String intern();
-
可见为native方法,在底层实现,通过文档,可以得知:
-
当调用 intern方法时,如果 池已经包含一个等于此String对象的字符串 (用equals(oject)方法确定),则返回池中的字符串,否则,将此String对象添加到池中,并返回此String对象的引用。( 注意:这是JDK1.7及以后intern方法)
-
它的大体实现结构就是: JAVA 使用 jni 调用c++实现的StringTable的intern方法, StringTable的intern方法跟Java中的HashMap的实现是差不多的, 只是不能自动扩容。默认大小是1009。
-
要注意的是,String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。
-
在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:-XX:StringTableSize=99991
-
JDK1.6,1.7及之后,intern()方法的区别
- JDK1.6中:intern方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用;
- JDK1.7及之后版本,intern的实现不会再复制实例,而是在常量池中记录首次出现的实例引用,即heap中的地址。
代码分析1
同样是文章中的代码:
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2); //jdk1.6下false,jdk1.7及以后false
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4); //jdk1.6下false,jdk1.7及以后true
}
打印结果是
jdk6 下false false
jdk7 下false true
- 分析:
- JDK1.6 & 1.7中,s==s2 false:
- 首先,String s = new String(“1”),会创建两个对象,一个位于字符串常量区,一个位于heap内存中,
- s会指向heap内存中的对象地址;
- 之后 s.intern(),因为字符串常量池中已存在 “1” ,因此不会再次添加;
- 而 String s2 = “1” 语句,会先在字符串常量池中寻找 “1”,发现已存在,返回字符串常量池中的 “1” 地址,s和s2这两
个地址必然不同;
- JDK1.6中,s3==s4 false & JDK1.7中,s3==s4 true:
- String s3 = new String(“1”) + new String(“1”):
在字符串常量池中已存在 “1”,不会添加,s3会指向heap中新生成的内容为"11"的String对象; - s3.intern():
JDK1.6中intern方法会把首次遇到的字符串实例"11"复制到永久代中,在永久代中生成一个新的"11"实例;
而JDK1.7中,在常量池中不会生成实例,只会存入heap中的"11"引用地址; - String s4 = “11”:
“11"已经存在于字符串常量池中,s4会指向字符串常量池中的"11”; - 因此,在JDK1.6中,s3指向的是heap中的"11"对象,而s4指向的是intern方法复制到字符串常量池中的"11",这两个地址必然不同,所以为false;(此处美团那篇文章个人认为分析有误,如果1.6和1.7同样是将第一次出现的引用放入字符串常量池中,即使字符串常量池位于永久代中,也会返回true,因为地址相同);
- JDK1.7中由于已经将第一次出现"11"的引用放入字符串常量池中,因此s3直接指向heap中的"11",s4会指向字符串常量池中的"11",但是字符串常量池中"11"是heap中"11"的地址,所以s3,s4引用地址相同,返回true;
- String s3 = new String(“1”) + new String(“1”):
- JDK1.6 & 1.7中,s==s2 false:
代码分析2
将s3.intern();语句下调一行,放到String s4 = “11”;后面。将s.intern(); 放到String s2 = “1”;后面。
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2); //jdk1.6下false,jdk1.7及以后false
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4); //jdk1.6下false,jdk1.7及以后false
}
打印结果是:
jdk6 下false false
jdk7 下false false
- 分析:
相信大家看到这里,也能分析出来原因,此处我只把JDK 1.7 s3==s4 false原因说一下:- 同上,String s3 = new String(“1”) + new String(“1”):
会在heap中生成内容为"11"的String对象; - String s4 = “11”:
此处"11"第一次出现,因此会将"11"实例放入字符串常量池中; - s3.intern():
入池,发现字符串常量池中已存在"11",因此不会将heap中的"11"引用地址放入; - s3是heap中的"11"引用地址,s4为字符串常量池中"11"地址,必然为false;
- 同上,String s3 = new String(“1”) + new String(“1”):