1.1 题目

  1. package s03.e01;
  2. public class StringPoo158Demo {
  3. public static void main(String[] args) {
  4. String str1 = new StringBuilder("58").append("tongcheng").toString();
  5. System.out.println(str1);
  6. System.out.println(str1.intern());
  7. System.out.println(str1.intern() == str1);
  8. System.out.println();
  9. String str2 = new StringBuilder("ja").append("va").toString();
  10. System.out.println(str2);
  11. System.out.println(str2.intern());
  12. System.out.println(str2.intern() == str2);
  13. }
  14. }

image.png
image.png

1.2 方法区和运行时常量池溢出

由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。前面曾经提到 HotSpot 从 JDK 7 开始逐步“去永久代”的计划,并在 JDK 8 中完全使用元空间来代替永久代的背景故事,在此我们就以测试代码来观察一下,使用“永久代””还是“元空间”来实现方法区,对程序有什么实际的影响。
String:intern() 是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象的引用;否则,会将此 String 对象包含的字符串添加到常量池中,并且返回此String 对象的引用。在 JDK 6 或更早之前的 HotSpot 虚拟机中,常量池都是分配在永久代中,我们可以通过 -XX: PermSize 和 -XX:MaxPermSize 限制永久代的大小,即可间接限制其中常量池的容量。

1.3 题目解析

按照代码结果,java 字符串答案为 false,必然是两个不同的 java,那另外一个 java 字符串如何加载进来的?
有一个初始化的 java 字符串(JDK 出娘胎自带的),在加载 sun.misc.Version 这个类的时候进入常量池

1.3.1 System 代码解析

image.png
image.png
image.png
image.png
image.png

1.3.2 根加载器提前部署加载 rt.jar

image.png

1.3.3 总结

这段代码在 JDK 6 中运行,会得到两个 false,而在 JDK 7 中运行,会得到一个 true 和一个 false。产生差异的原因是,在 JDK 6 中,intern() 方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由 StringBuilder 创建的字符串对象实例在 Java 堆上,所以必然不可能是同一个引用,结果将返回 false。
而 JDK7(以及部分其他虚拟机,例如 JRockit)的 intern() 方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到 Java 堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此 intern() 返回的引用和由 StringBuilder 创建的那个字符串实例就是同一个。而对 str2 比较返回 false,这是因为“java”这个字符串在执行 String-Builder.toString() 之前就已经出现过了,字符串常量池中已经有它的引用,不符合 intern() 方法要求“首次遇到”的原则,“58tongcheng”这个字符串则是首次出现的,因此结果返回 true
sun.misc.Version 类会在 JDK 类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue)做默认初始化,此时被 sun.misc.Version.launcher 静态常量字段所引用的“java”字符串字面量就被 intern 到 HotSpot VM 的字符串常量池——StringTable 里了。