故事起源于书籍《深入理解Java虚拟机》,案例如下:
public class RunTimeConstantPoolOOM {
public static void main(String[] args) throws Throwable {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
这段代码在JDK1.6中执行会得到两个false,在JDK1.7中会得到一个true和一个false。笔者未在JDK1.7执行,选择的是在JDK1.8中执行,结果是和1.7是一样的。书中是这样解释产生差异的原因:在JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。而JDK1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。
笔者不知道各位其他读者在阅读此书时,对这段话是不是一下子就明白了,反正笔者是有些不太明白的,尤其是JDK1.7下str2返回false这段。所以笔者觉得对这部分内容需要学习一下。
在学习之前,先了解下如下基础知识:String.intern()方法作用、虚拟机内存划分、永久代和元空间。
String.intern()方法作用
String.intern()是一个native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。查阅openjdk6、openjdk8的intern方法源码(hotspot\src\share\vm\classfile\symbolTable.cpp)实现可以证明上述这句话。
虚拟机内存划分
图一 虚拟机内存区域
图二 JDK1.6之前虚拟机内存区域细化
图三 JDK1.6之前虚拟机内存区域继续细化
程序计数器:程序计数器是一块较小的内存,它可以看做是当前线程执行的字节码行号指示器。在虚拟机概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。由于JVM的多线程就是通过轮流切换并分配处理器的执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程的指令。因此为了线程切换后能恢复到正确的执行位置,每条线程都有自己的程序计数器。
虚拟机栈:虚拟机栈和程序计数器一样,也是线程私有的。虚拟机栈描述的是Java方法执行的内存模型,即每个方法在执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。其中局部变量表存放的是编译器可知的各种基本数据类型和对象引用。
本地方法栈:本地方法栈与虚拟机栈所发挥的作用非常类似,区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
堆:Java堆是Java虚拟机所管理的内存最大的一块,Java堆被所有的线程共享。所有的对象实例以及数组都要在堆上分配。从内存回收的角度看,堆可以细分为新生代和老年代(见图二、图三堆空间的介绍)。再细化点,新生代又可以分为Eden、From Survivor、To Survivor(见图二、图三堆空间的介绍)。
方法区:方法区和堆一样,是各个线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据,对于习惯在HotSpot虚拟机上开发的开发者来说,方法区被习惯性的称为“永久代”(见图三的方法区),JDK1.8后已经移除永久代,取而代之的是元空间。
运行时常量池:运行时常量池是方法区的一部分。用于存放编译器生成的各种字面量和符号引用。
永久代和元空间
以HotSpot虚拟机为例,在1.6、1.7和1.8版本中,对于堆的实现没有太大的差异,主要分为年轻代和年老代。但是对于方法区(即永久代)的实现存在着差异,移除永久代的工作从1.7开始,但是并未完全移除,永久代仍然存在1.7中,但是其中的符号引用、字面量和类的静态变量都转移到了堆,直到1.8才完全移除永久代,取而代之的是元空间。以运行时常量池为例,调用方法String.intern(),在各个版本指定JVM参数,执行的结果略有差异,案例代码如下:
public static void main(String[] args) throws Throwable {
List<String> list = new ArrayList<String>();
String base = "string";
for (int i=0;i< Integer.MAX_VALUE;i++){
String str = base + base;
base = str;
list.add(str.intern());
}
}
在1.6指定参数:-XX:PermSize=10m -XX:MaxPermSize=10m,执行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.lingjiango.oom.RunTimeConstantPoolOOM.main(RunTimeConstantPoolOOM.java:29)
在1.7指定参数:-XX:PermSize=10m -XX:MaxPermSize=10m,执行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2367)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:130)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:114)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:415)
at java.lang.StringBuilder.append(StringBuilder.java:132)
at com.lingjiango.oom.RunTimeConstantPoolOOM.main(RunTimeConstantPoolOOM.java:27)
在1.8指定参数:-XX:PermSize=10m -XX:MaxPermSize=10m,执行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(Unknown Source)
at java.lang.AbstractStringBuilder.append(Unknown Source)
at java.lang.StringBuilder.append(Unknown Source)
at com.lingjiango.oom.RunTimeConstantPoolOOM.main(RunTimeConstantPoolOOM.java:28)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10m; support was removed in 8.0
从上述结果可以看出,1.6下,会出现“PermGen Space”的内存溢出,而在 1.7和 1.8 中,会出现堆内存溢出,并且1.8中提示PermSize 和 MaxPermSize已经不再支持。因此,可以大致验证 1.7和1.8 将字面量由永久代转移到堆中,并且 1.8 中已经不存在永久代的结论。
有了以上概念,再画图理解如上第二段话。
图四 JDK1.6示意图
图五 JDK1.8示意图
从图四可以看出s1和s1.intern不是一个对象,s2和s2.intern不是一个对象,所以结论是false,而图五中s1和s1.intern是一个对象,s2和s2.intern不是一个对象,所以前者结论为true,后者为false。但是笔者在理解这段话的时候,还有一个点没理解到的就是为什么“计算机软件”是第一次出现,而“java”却不是第一次出现呢?如果只是从这段代码中是无法理解这句话的,必须从全局来看,虚拟机在启动加载的时候,自动调用System类,System类会调用sun.misc.Version.init(),而在Version方法中,就有字符串常量“java”,所以在这段代码中,“java”不是第一次出现。
private static final String launcher_name = "java";
private static final String java_version = "1.8.0_181";
private static final String java_runtime_name = "Java(TM) SE Runtime Environment";
private static final String java_profile_name = "";
private static final String java_runtime_version = "1.8.0_181-b13";
参考资料:
《深入理解Java虚拟机》
http://www.importnew.com/14142.html
https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf
https://docs.oracle.com/javase/specs/jvms/se6/html/VMSpecTOC.doc.html