早上看群友们在讨论,出现了Java字符串究竟在哪里的话题,粗略目睹虚拟机的菜鸡本人起了兴趣,研究了一下整理出来这个博文
字符串究竟存在哪里
这里总结了大家同意的说法:
- 如果使用常量的方式,该对象将被存储在常量池(永久代)
- 如果使用new的方式,该对象将被存储在堆
下面的代码揭示了情况:
- 使用常量的形式,为同一个对象
- 使用new的方式,为不同的对象
- 即使调用了常量创建的对象的方法,该对象也并不会发生什么变化:说明就是一个对象,而不是一组数组
public class TestString {
public static void main(String[] args) {
String a="Aa";
String b="Aa";
System.out.println(a==b);
String c=new String("Aa");
System.out.println(b==c);
b.charAt(0);
String d=new String("Aa");
System.out.println(d==c);
System.out.println(b==a);
}
}
/**
true
false
false
true
Process finished with exit code 0
**/
简单的规则就是这样了,希望浅显了解看到这里就足够了
但反观具体的实现,又会觉得奇妙,比如Java的字符串常量池,intern方法,下面详细说明
更深入一步的了解
在开始之前,各位先看这个String中神奇的方法
String.intem();
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class <code>String</code>.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this <code>String</code> object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this <code>String</code> object is added to the
* pool and a reference to this <code>String</code> object is returned.
* <p>
* It follows that for any two strings <code>s</code> and <code>t</code>,
* <code>s.intern() == t.intern()</code> is <code>true</code>
* if and only if <code>s.equals(t)</code> is <code>true</code>.
* <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 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。
先记住这个奇妙的方法,建议你思考一下这个逻辑。
然后了解一下JDK1.6与JDK1.7中常量池的区别
先看看JDK1.6
在JDK1.6中,常量池是一块独立的区域,jdk6中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的。
有点人可能不是很懂
这里再换种说法:
“String”类型的String,存放在常量池,同时常量池与Java堆是两个不同的部分。
也就是说,
- 存放在常量池和存放在堆,实现方式都不一样。
- 常量池默认4M,所以存在消耗尽,抛出
java.lang.OutOfMemoryError: PermGen space
来到JDK1.7
在JDK1.7中,取消了这个称为perm的区域,常量池就是堆,在常量池中的String对象和堆的String对象是存在与同一区域的。在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称 jdk8 已经直接取消了 Perm 区域,而新建立了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。
问题发现
综合上述,不难发现,在JDK1.6和JDK1.7中运行同样的代码,结果缺会有不同
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
在JDK1.6中,常量池和堆是明显分隔的
于是我们可以推断:
- s对象new出,于是在堆中
- s2对象为常量,存在与常量池
- s与s2存储的位置都不同,s!=s2
- s3为两个字符串对象相加,调用了intern方法,但是存储位置都不同,s3必然不同于s4
结果为false false
在JDK1.7中,由于不存在了perm这个特殊的区域
- s对象new出,于是在堆中
- s2对象为常量,存在与常量池
- s与s2存储的位置都不同,s!=s2
- s3对象值为“11”,但是是由两个"1"拼接,所以常量池中只会存在"1"而不会存在"11"(这点很重要)
- s3intern方法,在常量池中生成了"11"。因为此时常量池中不存在“11”字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个 “11” 的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
- s4直接使用常量池"11"
- s3==s4
参考文章
https://www.baeldung.com/java-string-pool
https://www.cnblogs.com/holten/p/5782596.html
https://www.baeldung.com/java-string-pool