本文是在阅读《深入理解java虚拟机》2.4.3节时受到启发而写的,书中对这个知识点讲得也比较清楚了,这里自己再总结一下,加深理解。
文中根据书中的内容加入了自己的一些理解,如有理解不对的地方,欢迎讨论交流。
首先来看如下两个测试用例,执行完成后分别输出什么?
答案是testStringIntern1()输出true,testStringIntern2()输出false,如果是在jdk6中测试,答案分别是false, false,为什么会这样,如果明白了这个问题,对jvm内存结构会有个更清晰的认识。
public class StringTest {
@Test
public void testStringIntern1() {
String a = new StringBuilder().append("I love ").append("java").toString();
String intern = a.intern();
System.out.println(intern == a);
}
@Test
public void testStringIntern2() {
String a = new StringBuilder().append("I love ").append("java").toString();
String b = "I love java";
String intern = a.intern();
System.out.println(intern == a);
}
}
我们知道,jvm内存包含堆区和方法区两个区域(这里不考虑其他区域),堆区用来存放对象实例,方法区保存类型信息,常量,静态变量,即时编译后的代码缓存等。方法区是jvm规范中定义的一个jvm内存区域,jvm规范只是规定了方法区应该存放什么类型的数据,但是这些数据真正要存在哪里,要看具体用的哪个虚拟机,不同的jvm可能会有不同的实现。
我们先来看一下《深入理解java虚拟机》是怎么描述hotspot虚拟机的方法区的。
从上面的描述我们可以知道,hotspot虚拟机中的方法区具体实现是在永久代(jdk7及之前)或元空间(jdk7之后),但是上面这段话说得有点模糊,注意文中说的,“到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出”,但是这里并没有说字符串常量池和静态变量移出是移出到了哪里,实际上是移到了堆中,也就是字符串常量和静态变量实际上是保存在堆中,虽然从概念是来说它们是属于方法区的数据。注意还有一句话,“还剩余的内容(主要是类型信息)全部移到了元空间中”,也就是说jdk8及之后,字符串常量和静态变量是保存在堆中,类型信息保存在元空间中,但是字符串常量、静态变量、类型信息在概念上都是属于方法区的。
这里有必要再讲一个intern方法的作用,以及intern方法在jdk6及jdk7中的不同。
jdk中对intern方法的描述。
这里引用一下这篇文章(https://www.cnblogs.com/itplay/p/11137526.html)中对intern方法的解释,在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,并在字符串池中增加一指向这个新创建的实例。在 JDK 1.7 中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
我们还需要知道字符串常量池中存的是什么。首先需要明白,字符串常量池中保存的不是字符串对象,而是字符串对象的引用。那字符串对象保存在哪里呢?这个要看这个字符串是怎么生成的以及jdk的版本。具体差异如下表所示。
jdk版本 | 字符串定义方式 | 结果 |
6 | String a = "a" | 在永久代内存中创建了一个值为"a"的字符串对象,并在字符串常量池中添加一个引用指向这个对象。 |
String a = new String("a") | 在永久代内存中创建了一个值为"a"的字符串对象,并在字符串常量池中添加一个引用指向这个对象。 同时在堆中创建一个值为"a"的字符串,变量a指向堆中的这个字符串对象。 |
|
7 | String a = "a" | 在堆内存中创建了一个值为"a"的字符串对象,并在字符串常量池中添加一个引用指向这个对象。 |
String a = new String("a") | 在堆内存中创建了一个值为"a"的字符串对象,并在字符串常量池中添加一个引用指向这个对象。 同时在堆内存中再创建一个值为"a"字符串对象,变量a指向这个对象。 |
有了上面的知识背景,下面我们来分析testStringIntern1方法。
执行到下面这一句时,java堆内存中会有一个String对象,值为“I love java”,变量a指向这个对象,由于没有通过字面量方式定义,所以在字符串常量池中是没有一个指针是指向值为"I love java"的字符串对象的。
String a = new StringBuilder().append("I love ").append("java").toString();
接着,执行下面的intern方法时,程序会去字符串常量池中查找有没有和变量a,也就是“I lova java”相等(equals方法相等)的字符串,此时字符串常量池中还是没有的。但是由于堆内存中已经有了一个值为"I love java"的字符串对象,所以在jdk7及以后的版本中会直接复用堆内存中的这个字符串对象,把这个字符串对象的引用放到字符串常量池中,这样就相当于字符串常量池中有了"I love java"这个字符串对象。所以返回的结果和a变量是指向同一个字符串对象的,所以intern==a结果为true。在jdk6时的hotspot虚拟机,由于字符串常量池是在永久代中实现,所以调用intern时会在永久代查找“I love java”,找不到,就会把堆内存中的“I love java”字符串对象拷贝一个到永久代中,然后返回永久代中的引用,那么intern和a用==比较就会返回false。
String intern = a.intern();
接下来分析testStringIntern2方法。
第一句代码和上面一样,执行完后会在堆内存中创建一个"I love java"的字符串对象,但是要注意这个对象我们不是通过字面量形式定义的,它和其他普通java对象是一样的,仅仅只是堆中的一个对象,和字符串常量池之间没有任何关系。
下面这句代码定义了一个字符串字面量,字面量形式的字符串对象在jdk7中会放在堆内存中,在jdk6会放在永久代,但是不管放在哪里,此时字符串常量池中已经有了一个引用,指向这个值为"I love java"的字符串对象,这个对象和上面a指向的不是同一个对象。
String b = "I love java";
然后调用a.intern()方法时,同样,程序会在字符串常量池中查找有没有“I love java”字符串对象,发现已经有了,也就是我们上面定义的b,于是返回已有的这个字符串对象,其实就是返回b指向的那个对象的引用,由于b指向的对象和a不是同一个,所以intern == a为false。大家可以试验一下,b == intern是为true的,也就是a.intern()返回的就是b指向的那一个对象的引用。
总结:
intern方法在不同jdk中不同的表现是学习和理解jvm内存结构很好的一个例子,好好理解一下上面的测试用例对理解jvm内存结构会很有帮助。
参考:
《深入理解java虚拟机》
https://www.cnblogs.com/itplay/p/11137526.html