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)
';