近日和同事讨论到new String("123") 以及String.intern()的相关问题,这里做个简单记录。
直接进入分析阶段~
首先我们得了解jvm的内存结构,这里引用journaldev中的图
public class Memory {
public static void main(String[] args) { // Line 1
int i=1; // Line 2
Object obj = new Object(); // Line 3
Memory mem = new Memory(); // Line 4
mem.foo(obj); // Line 5
} // Line 9private void foo(Object param) { // Line 6
String str = param.toString(); Line 7
System.out.println(str);
} // Line 8}
这段代码在jvm内存分配如下(jdk1.7以后)
在jdk1.7之前串常量存储在方法区的PermGen Space。在jdk1.7之后,字符串常量重新被移到了堆中。字符串常量可以看做图中的String Pool 区。
简述:
String str = "123"; 这里str变量在栈中,指向String Pool 区的具体“123”对象。
String str2 = new String("abc");这句创建了几个对象呢?我们看下源码
/** * Initializes a newly created {@code String} object so that it represents * the same sequence of characters as the argument; in other words, the * newly created string is a copy of the argument string. Unless an * explicit copy of {@code original} is needed, use of this constructor is * unnecessary since Strings are immutable. * * @param original * A {@code String} */ public String(String original) { this.value = original.value; this.hash = original.hash; }
the newly created string is a copy of the argument string 这句翻译大意:“新创建的字符串是参数字符串的副本”
那么参数字符串是否创建就要视情况而定。
- 如果“abc” 在String Pool 区中不存在,则会在String Pool中新建一个字符串对象,然后new String(“abc”) 又会在堆内存中创建一个对象。
- 如果“abc”在String Pool区存在,则只会在堆中创建一个对象。
然后说下String.intern();这里intern()源码不贴出来了,里面关键语句翻译大意:“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。
《深入理解java虚拟机》写到:
因为JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串的实例的引用,而StringBulder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。
在JDK1.7中,intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此返回的是引用和由StringBuilder.toString()创建的那个字符串实例是同一个。
jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:
- 将String常量池 从 Perm 区移动到了 Java Heap区
String#intern
方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
基于以上的的几点我们来几个测试验证上面论述:
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
这里针对jdk7之后内存模型进行解释:
- String s = new String("1"); 首先会在常量池中创建"1"的对象,并且在堆中创建s的引用对象,s.intern() 会检查常量池中是否有s指向的对象值"1",由于常量池中已经有了,所有会返回常量池中的值(但是这里并没有用到返回值),String s2 = "1"; 这里s2指向常量池中的值,而s指向堆中的值,所有s == s2 为false;
- String s3 = new String("1") + new String("1"); 这里两个new String("1"); 并且"1"在常量池中是有的(此时常量池中并没有值"11"),s3 指向堆中对象的引用。
- s3.intern() 首先会检查s3所对应的值"11" 是否在常量池中存在,由于常量池中并不存在,所有会把指向堆中字符串值"11"的引用s3 也存放一份到常量池中(即常量池中存放一份和s3相同指向的引用)(jdk6 会在常量池中生成一个 “11” 的对象)
- String s4 = "11"; 这里会与常量池中做"equal"比较,由于常量池中已存在,所有s4指向常量池中的引用,即 s3 == s4
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2); // jdk6 false, jdk7 false
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4); // jdk6 false, jdk7 false
- 第一段代码和第二段代码的改变就是
s3.intern();
的顺序是放在String s4 = "11";
后了。这样,首先执行String s4 = "11";
声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行s3.intern();
时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。 - 第一段代码和第二段代码中的 s 和 s2 代码中,
s.intern();
,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1");
的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。
String s = new String(“abc”);
String s1 = “abc”;
String s2 = new String(“abc”);
System.out.println(s == s1.intern()); //false
System.out.println(s == s2.intern()); //false
System.out.println(s1 == s2.intern()); // true
- String s = new String(“abc”); 如果常量池不存在"abc"对象则会创建"abc",而s 指向堆中对象;
- String s1 = "abc"; s1指向常量池中的对象,s1.intern() 返回常量池中的对象,所有s == s1.intern // false。其余同理分析
以下是部分参考文章,感谢
https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
https://www.journaldev.com/4098/java-heap-space-vs-stack-memory