故事起源于书籍《深入理解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

https://www.cnblogs.com/snowwhite/p/9532311.html

https://www.cnblogs.com/paddix/p/5309550.html

JVM-String.intern()的更多相关文章

  1. 对于JVM中方法区,永久代,元空间以及字符串常量池的迁移和string.intern方法

    在Java虚拟机(以下简称JVM)中,类包含其对应的元数据,比如类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池,已确定的符号引用和虚方法表. 在过去(当自定义类加载器使用不普 ...

  2. 关于jvm中的常量池和String.intern()理解

    1. 首先String不属于8种基本数据类型,String是一个对象. 因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些特性. 2. ne ...

  3. JVM系列之:String.intern和stringTable

    目录 简介 intern简介 intern和字符串字面量常量 分析intern返回的String对象 分析实际的问题 G1中的去重功能 总结 简介 StringTable是什么?它和String.in ...

  4. JVM系列之:String.intern的性能

    目录 简介 String.intern和G1字符串去重的区别 String.intern的性能 举个例子 简介 String对象有个特殊的StringTable字符串常量池,为了减少Heap中生成的字 ...

  5. Java提高篇——理解String 及 String.intern() 在实际中的应用

    1. 首先String不属于8种基本数据类型,String是一个对象.   因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些特性. 2. ...

  6. 深入理解Java String#intern() 内存模型

    原文出处: codelog.me 大家知道,Java中string.intern()方法调用会先去字符串常量池中查找相应的字符串,如果字符串不存在,就会在字符串常量池中创建该字符串然后再返回. 字符串 ...

  7. String放入运行时常量池的时机与String.intern()方法解惑

    运行时常量池概述 Java运行时常量池中主要存放两大类常量:字面量和符号引用.字面量比较接近于Java语言层面的常量概念,如文本字符串.声明为final的常量值等. 而符号引用则属于编译原理方面的概念 ...

  8. string.intern

    在翻<深入理解Java虚拟机>的书时,又看到了2-7的 String.intern()返回引用的测试. 总结一句话: jdk1.7之前,调用intern()方法会判断常量池是否有该字符串, ...

  9. String学习之-深入解析String#intern

    引言 在 JAVA 语言中有8中基本类型和一种比较特殊的类型String.这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念.常量池就类似一个JAVA系统级别提供的缓存. 8 ...

  10. 深入解析String#intern

    转自:https://tech.meituan.com/in_depth_understanding_string_intern.html 深入解析String#intern john_yang ·2 ...

随机推荐

  1. 背水一战 Windows 10 (95) - 选取器: 自定义文件保存选取器

    [源码下载] 背水一战 Windows 10 (95) - 选取器: 自定义文件保存选取器 作者:webabcd 介绍背水一战 Windows 10 之 选取器 自定义文件保存选取器 示例1.演示如何 ...

  2. C#通过COM组件操作IE浏览器(一):打开浏览器跳转到指定网站

    简介Internet Explorer对象模型 1.属性 属性 类型 描述 Application Object 返回对Internet Explorer对象的引用. Busy Boolean 返回一 ...

  3. 技术干货:实时视频直播首屏耗时400ms内的优化实践

    本文由“逆流的鱼yuiop”原创分享于“何俊林”公众号,感谢作者的无私分享. 1.引言 直播行业的竞争越来越激烈,进过2018年这波洗牌后,已经度过了蛮荒暴力期,剩下的都是在不断追求体验.最近正好在做 ...

  4. Android 关于解决MediaButton学习到的media控制流程

    问题背景:话机连接了头戴式的耳机,在通话过程中短按按钮是挂断电话,长按按钮是通话静音.客户需求是把长按改成挂断功能,短按是静音功能. android版本:8.1 在通话中,测试打印信息,可以看到but ...

  5. Spring Data Redis 详解及实战一文搞定

    SDR - Spring Data Redis的简称. Spring Data Redis提供了从Spring应用程序轻松配置和访问Redis的功能.它提供了与商店互动的低级别和高级别抽象,使用户免受 ...

  6. vmdk多文件合成单文件并导入

    如果创建时,目录中有多个vmdk文件,可以将其合成一个,方便导到其他地方运行,如图所示 win下cmd命令,找到安装vmware目录,合并的命令如下 vmware-vdiskmanager.exe - ...

  7. 1.numpy的用法

    numpy创建ndarray对象的三种方法 1.1.list转化 In [8]: import numpy as np In [9]: a = [1,2,3,4] In [10]: x1 = np.a ...

  8. vue 项目实战 (入门)

    环境搭建 安装NodeJS →箭头https://nodejs.org/en/ NPM是随同NodeJS一起安装的包管理工具. 检查环境是否安装成功: 打开一个命令提示符,有成功输出版本号则为安装成功 ...

  9. ArcSDE数据库连接(直连、服务连)与GT_Geometry存储配置图解

    众说周知,ArcSDE空间数据库引擎提供了两种连接数据库的方式.一是服务连接方式,一是直连方式.后者也是Esri所推崇的方式.但是,在客户的生产环境和开发商的开发环境中这两种方式都是有需求的.下面就以 ...

  10. Java 容器 & 泛型:四、Colletions.sort 和 Arrays.sort 的算法

    Writer:BYSocket(泥沙砖瓦浆木匠) 微博:BYSocket 豆瓣:BYSocket 本来准备讲 Map集合 ,还是喜欢学到哪里总结吧.最近面试期准备准备,我是一员,成功被阿里在线笔试秒杀 ...