JVM-String.intern()
故事起源于书籍《深入理解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()的更多相关文章
- 对于JVM中方法区,永久代,元空间以及字符串常量池的迁移和string.intern方法
在Java虚拟机(以下简称JVM)中,类包含其对应的元数据,比如类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池,已确定的符号引用和虚方法表. 在过去(当自定义类加载器使用不普 ...
- 关于jvm中的常量池和String.intern()理解
1. 首先String不属于8种基本数据类型,String是一个对象. 因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些特性. 2. ne ...
- JVM系列之:String.intern和stringTable
目录 简介 intern简介 intern和字符串字面量常量 分析intern返回的String对象 分析实际的问题 G1中的去重功能 总结 简介 StringTable是什么?它和String.in ...
- JVM系列之:String.intern的性能
目录 简介 String.intern和G1字符串去重的区别 String.intern的性能 举个例子 简介 String对象有个特殊的StringTable字符串常量池,为了减少Heap中生成的字 ...
- Java提高篇——理解String 及 String.intern() 在实际中的应用
1. 首先String不属于8种基本数据类型,String是一个对象. 因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些特性. 2. ...
- 深入理解Java String#intern() 内存模型
原文出处: codelog.me 大家知道,Java中string.intern()方法调用会先去字符串常量池中查找相应的字符串,如果字符串不存在,就会在字符串常量池中创建该字符串然后再返回. 字符串 ...
- String放入运行时常量池的时机与String.intern()方法解惑
运行时常量池概述 Java运行时常量池中主要存放两大类常量:字面量和符号引用.字面量比较接近于Java语言层面的常量概念,如文本字符串.声明为final的常量值等. 而符号引用则属于编译原理方面的概念 ...
- string.intern
在翻<深入理解Java虚拟机>的书时,又看到了2-7的 String.intern()返回引用的测试. 总结一句话: jdk1.7之前,调用intern()方法会判断常量池是否有该字符串, ...
- String学习之-深入解析String#intern
引言 在 JAVA 语言中有8中基本类型和一种比较特殊的类型String.这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念.常量池就类似一个JAVA系统级别提供的缓存. 8 ...
- 深入解析String#intern
转自:https://tech.meituan.com/in_depth_understanding_string_intern.html 深入解析String#intern john_yang ·2 ...
随机推荐
- 剑指offer面试题25:二叉树中和为某一值的路径
题目:输入一棵二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径.从根节点开始往下一直到叶节点所经过的节点形成一条路径. 解题思路:当使用前序遍历的方式访问某一节点时,把该节点添加到路径上 ...
- 2016 安全圈玩起了直播,“学霸”带你玩转CTF_i春秋学院
2016安全圈玩起了直播,“学霸”带你玩转CTF_i春秋学院 从小就很羡慕学霸的脑子,总有很简单很便捷的方法解出难题来,所以对于他们的笔记总会疯狂地想占有和copy.那么,对CTF大神自己总结出来的赛 ...
- C#8个常用的字符串的操作
1.根据单个分隔字符用split截取 string st="GT123_1"; string[] sArray=st.split('_'); 输出:sArray[0]=" ...
- 音频处理库—librosa的安装与使用
序言 Librosa是一个用于音频.音乐分析.处理的python工具包,一些常见的时频处理.特征提取.绘制声音图形等功能应有尽有,功能十分强大.本文主要介绍librosa的安装与使用方法. 一.lib ...
- 阿里妈妈前端团队出品的开源接口管理工具RAP第二代 http://rap2.taobao.org
RAP2-DELOS 开源社区版本 (后端API服务器) 项目地址:https://github.com/thx/rap2-delos RAP2是在RAP1基础上重做的新项目,它包含两个组件(对应两个 ...
- angular整合环信webIM
此处有两大坑: 1.下载easemob-websdk此npm包时,并没有下载strophe.js.crypto-js.underscore这三个包,需要自己手动下载. 2.如下方标红位置所示,需要自己 ...
- iReport 5.6.0 组件面板为空 get小技巧
问题描述 本人使用的是iReport 5.6.0版本,正常安装,打开后,创建了一个报表实例,但是,菜单栏--> 工具--> 组件面板 为空.效果如下: 这就尴尬了,没有组件面板,还怎么绘制 ...
- 课程五(Sequence Models),第三周(Sequence models & Attention mechanism) —— 0.Practice questions:Sequence models & Attention mechanism
- MapReduce中的Join
一. MR中的join的两种方式: 1.reduce side join(面试题) reduce side join是一种最简单的join方式,其主要思想如下: 在map阶段,map函数同时读取两个文 ...
- api网关揭秘--spring cloud gateway源码解析
要想了解spring cloud gateway的源码,要熟悉spring webflux,我的上篇文章介绍了spring webflux. 1.gateway 和zuul对比 I am the au ...