String放入运行时常量池的时机与String.intern()方法解惑
最后更新于:2022-04-01 12:01:56
### 运行时常量池概述
Java运行时常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(包名+类名)
- 字段的名称和描述符
- 方法的名称和描述符
### 运行时常量池位置
运行时常量池在**JDK1.6及之前版本的JVM中是方法区的一部分**,而在HotSpot虚拟机中方法区放在了”永久代(Permanent Generation)”。所以运行时常量池也是在永久代的。
但是**JDK1.7及之后版本的JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池**。
**本文主要解惑String对象(即文本字符串)何时放入常量池,不涉及上述三类符号引用常量和其他非String常量值。而且本文只讨论主流的HotSpot虚拟机。**
### String何时放入常量池
记住一句话:**直接使用双引号声明出来的String对象会直接存储在常量池中。**
### 代码一:
~~~
String a = "计算机软件";
~~~
分析:因为计算机软件五个字直接使用了双引号声明,故JVM会在运行时常量池中首先查找有没有该字符串,有则直接返回该字符串在常量池中的引用;没有则直接在常量池中创建该字符串,然后返回引用。**此时,该句代码已经执行完毕,不会在java Heap(堆)中创建内容相同的字符串。该字符串只在常量池中创建了一个String对象。**
### 代码二:
~~~
String a = new String("计算机软件");
~~~
分析:该行代码生成了两个String对象(Stack(栈)中的对象引用不在讨论范围内):第一步,因为计算机软件五个字直接使用了双引号声明,故JVM会在运行时常量池中首先查找有没有该字符串,有则进入第二步;没有则直接在常量池中创建该字符串,然后进入第二步。第二步:在常量池中创建了一个String对象之后,由于使用了new,JVM会在Heap(堆)中创建一个内容相同的String对象,然后返回堆中String对象的引用。**该行代码分别在常量池和堆中生成了两个内容相同的String对象。**
### 代码三:
~~~
String a = "计算机" + "软件";
~~~
分析:由于JVM存在编译期优化,对于两个直接双引号声明的String的+操作,JVM在编译期会直接优化为“计算机软件”一个字符串,故该行代码同**代码一**。
### 代码四:
~~~
String b = "计算机";
String a = b + "软件";
~~~
分析:由于b是一个String变量,编译期无法确定b的值,故不会优化为一个字符串。即使我们知道b的值,但JVM认为它是个变量,变量的值只能在运行期才能确定,故不会优化。运行期字符串的+连接符相当于new,故该行代码在Heap中创建了一个内容为“计算机软件”的String对象,并返回该对象的引用。**至此,该代码执行完毕,因为没有直接双引号声明计算机软件这5个字的字符串,故常量池中不会生成计算机软件这5个字的字符串。但是会有“计算机”和“软件”这两个String对象,因为他们都用双引号声明了。**
### 代码五:
~~~
String final b = "计算机";
String a = b + "软件";
~~~
分析:该代码与代码四的唯一区别是将b声明为final类型,即为常量。故在编译期JVM能确定b的值,所以对+可以优化为“计算机软件”5个字的字符串。该代码的运行同**代码三和代码一**。
### 代码六:
~~~
String a = new String("计算机") + "软件";
~~~
分析:因为有new,该代码也无法编译期优化,故该行代码只是在Heap中生成了“计算机软件”字符串的String对象,在常量池中没有内容相同的对象生成。
### String.intern方法
### 概述
String.intern()是一个Native方法,它的作用是:如果运行时常量池中已经包含一个等于此String对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此String内容相同的字符串,并返回常量池中创建的字符串的引用。
### JDK1.7改变
当常量池中没有该字符串时,JDK7的intern()方法的实现不再是在常量池中创建与此String内容相同的字符串,而改为**在常量池中记录Java Heap中首次出现的该字符串的引用,并返回该引用**。
**验证代码:**
~~~
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println((str1.intern() == str1));
//JDK1.6:false
//JDK1.7:true
~~~
或
~~~
String b = "计算机";
String a = b + "软件";
System.out.println(a.intern() == a);
//JDK1.6:false
//JDK1.7:true
~~~
### 测试代码
请运行以下的代码看看你分析的结果和真正的运行结果是否一样,JDK1.6和1.7都要跑一遍,如果你都分析对了,那就是理解了。
~~~
//一次放开一个多行注释运行
/*
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);
*/
/*
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
*/
/*
//+连接但编译器不优化
String s1=new String("xy") + "z";
String s2=s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
*/
/*// 一般情况
String s1=new String("xyz") ;
String s2=s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
*/
/*//编译器优化
String s1 = "xy" + "z";
String s2 = s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
*/
~~~
说明:本文有部分内容摘抄了周志明大神的《深入理解Java虚拟机》一书,部分代码参考了网上多个博客的内容,仅用于学习。无意侵犯版权。
参考:
《深入理解Java虚拟机-JVM高级特性与最佳实践》周志明著
[ JVM常量池和八种基本数据及字符串](http://blog.csdn.net/rainnnbow/article/details/47018241)
[深入解析String#intern](http://www.importnew.com/14142.html)
[Java永久代去哪儿了](http://www.infoq.com/cn/articles/Java-PERMGEN-Removed)