Java之String字符串在JVM中的存储及其内存地址的问题
作者:我本是机械人
String字符串在JVM中的存储及其内存地址问题
概要
String 的内存地址问题是Java面试中常被问到的一个点,比如直接复制的String 和 new 出来的 String 有什么区别?
字符串拼接过程中地址是如何变化的?
等等。要想理清这些地址问题,我们首先应当知道 String 在JVM中是如何存储的。
1.String 对象在JVM中的存储
先给出定义:
字符串存放在方法区的常量池(Constant Pool)中,常量池是什么呢?
常量池在编译期间就会被生成,用于存放编译器生成的各种字面量和符号引用。
比如int a = 8;String a = “abc”; 这种里面的8和"abc"在编译阶段就会被放入常量池。
当然常量池在运行期间也可以被拓展,将新的常量放入池中,用的比较多的就是String 的 intern() 方法,后面会详细描述。
再来看这样一段简单的代码
String str = "aa"; String str1 = "aa"; String str2 = new String("aa"); String str3 = new String("aa"); System.out.println(str == str1);//true System.out.println(str2 == str3);//false System.out.println(str == str2);//false
为什么会是这样的结果呢?
按照程序的执行顺序,首先,“aa”作为一个字面量,也就是常量,会在编译期间被加入常量池,然后JVM将其在常量池中的地址赋给str;到了str1这里,JVM先在常量池中查找有没有“aa”这个常量,由于给str赋值的时候已经在常量池里创建过“aa”了,所以JVM直接返回这个地址给str1。因此str和str1的地址是一样的,结果为true。
来到str2和str3,这两个是new出来的String,是对象,对象存放在哪里呢?存放在堆中,所以本质上str2和str3存放的是这两个对象在堆中的地址。new了两次,他俩是不同的对象,所以str2和str3地址不相同,返回false。
现在再来看最后一个输出,str == str2?这俩一个存的是常量池中的地址,一个存的是堆中的地址,怎么可能相等嘛,返回false。
2.关于字符串拼接
来看这样一段代码
String a = "Hello2"; String b = "Hello"; final String c = "Hello"; String d = b + "2"; String e = c + "2"; String f = "Hello" + "2"; System.out.println(a==d);//false System.out.println(a==e);//true System.out.println(a==f);//true
首先a、b、c都是字面量直接赋值,所以现在常量池中有 “Hello2” 和 "Hello"两个字符串。
根据上面的结果我们可以得到如下两个结论:
(1)字符串相加的时候,都是静态字符串相加的结果会添加到常量池,如果常量池中有这个结果则直接返回其引用;如果没有,则创建该字符串再返回引用。因为现在常量池已经有 “Hello2” 了,所以e和f的值其实都是常量池中 “Hello2” 的引用,a、e、f都是相等的。
(2)字符串相加的时候,如果其中含有变量,如d中的b,则不会进入常量池中。在IDEA中DeBug,我们强制进入 String d = b + “2”; 这条语句,会发现程序底层其实是先创建了一个StringBuffer,然后调用append()方法,把b和"2"加入该StringBuffer,然后调用toString()返回拼接后的字符串,而最终的toString()源码长这个样子:
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
看到了吗!它返回了一个new的String!这个String的地址当然不会任何一个现有的对象相同了。关于字符串拼接,分清楚这两种情况即可。
3.关于intern()方法
前面我们提到过,new 出来的String不直接存放在常量池中,而intern()方法的作用就是把这个字符串加入到常量池中,然后返回这个字符串在常量池中的地址。
String str1 = "a"; String str2 = "b"; String str3 = "ab"; String str4 = str1 + str2; String str5 = new String("ab"); System.out.println(str5 == str3);//false System.out.println(str5.intern() == str3);//true System.out.println(str5.intern() == str4);//false System.out.println(str5.intern() == str4.intern());//true
调用str5.intern()时,JVM在常量池中查询有没有"ab"这个字符串,因为在给str3赋值时已经创建过,所以直接返回其地址。
str4由两个变量相加得到,所以也相当于是new出来的。
还有一个需要注意的点就是:
如果只是调用str5.intern(),那str5本身并不会改变,还是存放的堆里的地址,想让str5存放常量池中的地址需要把str5.intern()的返回值再赋给str5。
可以做如下测试:
String str3 = "ab"; String str5 = new String("ab"); str5.intern(); System.out.println(str5 == str3);//false str5 = str5.intern(); System.out.println(str5 == str3);//true
Java中字符串存储在JVM的哪部分?
1、 java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;
2、 java7中,static变量从永久代移到堆中;
3、 java8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中
现在总结一下:基本类型的变量数据和对象的引用都是放在栈里面的,对象本身放在堆里面,显式的String常量放在常量池,String对象放在堆中。
常量池的说明
常量池之前是放在方法区里面的,也就是在永久代里面的,从JDK7开始移到了堆里面。
这一改变我们可以从oracle的release version的notes里的** Important RFEs Addressed in JDK 7 **看到。
Area: HotSpot
Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.
RFE: 6962931
String内存位置说明
显式的String常量
String a = "holten"; String b = "holten";
- 第一句代码执行后就在常量池中创建了一个值为holten的String对象;
- 第二句执行时,因为常量池中存在holten所以就不再创建新的String对象了。
- 此时该字符串的引用在虚拟机栈里面。
String对象
String a = new String("holtenObj"); String b = new String("holtenObj");
- Class被加载时就在常量池中创建了一个值为holtenObj的String对象,第一句执行时会在堆里创建new String("holtenObj")对象;
- 第二句执行时,因为常量池中存在holtenObj所以就不再创建新的String对象了,直接在堆里创建new String("holtenObj")对象。
验证一下
/** * Created by holten.gao on 2016/8/16. */ public class Main { public static void main(String[] args){ String str1 = "高小天"; String str2 = "高小天"; System.out.println(str1==str2);//true String str3 = new String("高大天"); String str4 = new String("高大天"); System.out.println(str3==str4);//false } }
返回结果:
true
false
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。