故事起源于书籍《深入理解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. metools,个人工具站点分享

    需要[加密/解密][编码/解码][生成二维码]的时候不用再进百度点广告~ 也不需要去收藏夹找网址~ 我的目的大概就是如此. 项目地址:https://github.com/yimogit/metool ...

  2. Python 操作excel之 openpyxl模块

    1. 安装 pip install openpyxl 想要在文件中插入图片文件,需要安装pillow,安装文件:PIL-fork-1.1.7.win-amd64-py2.7.exe · font(字体 ...

  3. 【mysql注入】mysql注入点的技巧整合利用

    [mysql注入]mysql注入点的技巧整合利用 本文转自:i春秋社区 前言: 渗透测试所遇的情况瞬息万变,以不变应万变无谓是经验与技巧的整合 简介: 如下 mysql注入点如果权限较高的话,再知道w ...

  4. Hive数据仓库之快速入门

    Hive定位:ETL(数据仓库)工具 将数据从来源端经过抽取(extract).转换(transform).加载(load)至目的端的工具,如像:kettle 有关Hive数据导入导出mysql的问题 ...

  5. 弹性盒子模型属性之flex-shrink

    上一次,我们已经了解过flex-grow的具体用法后,这周,让我们一起来见一下flex-basis这个属性. flex-shrink 定义项目的缩小比例,默认值为1,注意前提是空间不足的情况下,项目缩 ...

  6. OAuth2简易实战(一)-四种模式

    1. OAuth2简易实战(一)-四种模式 1.1. 授权码授权模式(Authorization code Grant) 1.1.1. 流程图 1.1.2. 授权服务器配置 配置授权服务器中 clie ...

  7. requests 处理异常错误 requests.exceptions.ConnectionError HTTPSConnectionPool [Errno 10060]

    使用python requests模块调用vmallarg.vmall.com接口API时报如下错误: requests.exceptions.ConnectionError: HTTPSConnec ...

  8. 微信公众平台开发——为何不能在网页调用微信jsapi?

    说到这问题,相信大部分程序员老手都会轻蔑一笑,当然是跨域导致的啊!但是为了一些小白,我觉得还是很有必要再说一次的. 首先介绍什么是跨域,由于浏览器的同源策略,出于防范跨站脚本的攻击,禁止客户端脚本( ...

  9. Linux编程 13 (系统环境变量位置, 环境变量持久化)

    一.系统环境变量位置 在上章中,知道了如何修改系统环境变量,如PATH变量,以及创建自己的全局环境变量和局部环境变量.这篇学习怎么让环境变量的作用持久化.在此之前,先了解下系统环境变量文件会在哪些位置 ...

  10. 在微信小程序中使用图表

    前言:网上有许多的图表库,如:Echarts.Tau Charts.ChartJS等等,具体自行百度. 这次我们使用的是:Echarts 官方教程:点击查看 Echarts下载地址:飞机直达 1.下载 ...