故事起源于书籍《深入理解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. Vipe框架构思记

    准备着手写一个JAVA框架,基于公司目前的框架提取出来.当然公司现在的框架也是我搭建的.在这整理一下思路. 框架名称:Vipe AOP,IOC容器:Spring MVC:Spring MVC ORM: ...

  2. Canal学习笔记(客户端)

    前言 最近公司用到Canal来做从MySQL到Tidb的数据同步,用到HA模式Canal,记录一下HA模式的工作原理. Canal的架构模式 Canal是利用binlog日志来做数据同步,canal伪 ...

  3. 如果这样来理解HTTPS,一篇就够了!

    1.前言 可能有初学者会问,即时通讯应用的通信安全,不就是对Socket长连接进行SSL/TLS加密这些知识吗,干吗要理解HTTPS协议呢. 这其实是个误解:当今主流的移动端IM数据通信,总结下来无外 ...

  4. Android 代码混淆配置总结

    一.前言 为何需要混淆呢?简单的说,就是将原本正常的项目文件,对其类,方法,字段,重新命名,a,b,c,d,e,f…之类的字母,达到混淆代码的目的,这样反编译出来,结构乱糟糟的,看了也头大. 另外说明 ...

  5. rem计算

    //jquery实现 // $(function(){ // $(window).on("resize",function(){ // var width=$(window).wi ...

  6. 什么 是JavaScript中的变量? 部分2

    变量:是计算机存储数据的标识符 js中存储数据的方式 都是使用变量 js 中声明变量的方式都是var 存储数据,应该有对应的数据类型js中的字符串类型都用成对的单引号或者双引号包裹起来 变量 1. 变 ...

  7. deepin卸载mysql并安装设置mysql5.7

    mysql完全卸载以及安全安装 完全卸载 sudo apt purge mysql-* sudo rm -rf /etc/mysql/ /var/lib/mysql sudo apt autoremo ...

  8. Xamarin.Android 上中下布局

    xml代码: <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:and ...

  9. mysql 开发基础系列17 存储过程和函数(上)

    一. 概述 存储过程和函数是事先经过编译并存储在数据库中的一段sql语句集合,可以简化应用开发人员的很多工作,减少数据在数据库与应用服务器之间的传输,提高数据处理效率是有好处的.存储过程和函数的区别在 ...

  10. 用dos命令导出一个文件夹里面所有文件的名字(装逼利器)

    首先,当然是在相关的文件夹打开dos命令窗口. 然后,输入如下命令:dir/b >a.txt 如果你非常了解dos命令,那么你一定会觉得这个东西简单到爆,而且我的理解和猜想都有些无知. 但如果你 ...