JVM体系结构之七:持久代、元空间(Metaspace) 常量池==了解String类的intern()方法、常量池介绍、常量池从Perm-->Heap
字符串常量池从1.7以后,移到了heap中。(1.6在永久代,1.7以后移动到了heap中)。
一、intern()定义及使用
相信绝大多数的人不会去用String类的intern方法,打开String类的源码发现这是一个本地方法,定义如下:
public native String intern();
文档告诉我们该方法返回一个字符串对象的内部化引用。关于native方法详解见native关键字(本地方法)、 java调用so动态链接库
java.lang.String.intern():返回一个保留池字符串,就是一个在全局字符串池中有了一个入口。如果以前没有在全局字符串池中,那么它就会被添加到里面。
例如:
一个初始时为空的字符串池,它由类 String 私有的维护。 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。 它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
总结出来其意思如下:如果:s.intern()方法的时候,会将共享池中的字符串与外部的字符串(s)进行比较,如果共享池中有与之相等的字符串,则不会将外部的字符串放到共享池中的,返回的只是共享池中的字符串,如果不同则将外部字符串放入共享池中,并返回其字符串的句柄(引用)-- 这样做的好处就是能够节约空间。
众所周知:String类维护一个初始为空的字符串的对象池,当intern方法被调用时,如果对象池中已经包含这一个相等的字符串对象则返回对象池中的实例,否则添加字符串到对象池并返回该字符串的引用。
从程序的角度上怎么来看这个方法呢,我们假设有两个字符串s1,s2,当s1.equals(s2)时,s1.intern()==s2.intern(),也就是说这两个字符串在内存中使用的是同一个实例。
Java语言规范中定义了字符串文字以及更一般的常量表达式的值的字符串是被内部化的,以便它们共享同一个实例。我们试验一下下面代码:
String s1="你好,Java";
String s2="你好,"+"Java";
System.out.println(s1==s2);
System.out.println(s1.intern()==s2.intern());
这段代码将打印两个true,也就是说字符串s1和s2是共享同一个实例。不过前提是尽管使用了表达式,但是表达式中必须都是常量。
了解这个处理机制也可以让我们在用到字符串常量的时候了解如何节省这些字符串所占用的内存。
下面两个例子可以帮你:
public void inTest1() { System.out.println("begin inTest1()==========");
String a = "b";
String b = "b"; System.out.println(a == b); //true String c = "d";
String d = new String("d").intern();
System.out.println(c == d); //true
System.out.println("end inTest1()==========");
} public void inTest2() {
System.out.println("begin inTest2()==========");
String a = new String("abc");
String b = new String("ab");
b = b + "c";
System.out.println(a == b);//false
System.out.println(a == b.intern());//false
System.out.println(a.intern() == b);//false
System.out.println(a.intern() == b.intern()); //true
System.out.println("end inTest2()==========");
}
二、字符串常量池
2.1、 字面量和常量池初探
字符串对象内部是用字符数组存储的,那么看下面的例子:
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
会分配一个11长度的char数组,并在常量池分配一个由这个char数组组成的字符串,然后由m去引用这个字符串
用n去引用常量池里边的字符串,所以和m引用的是同一个对象
生成一个新的字符串,但内部的字符数组引用着m内部的字符数组
同样会生成一个新的字符串,但内部的字符数组引用常量池里边的字符串内部的字符数组,意思是和u是同样的字符数组
使用图来表示的话,情况就大概是这样的(使用虚线只是表示两者其实没什么特别的关系):
测试demo:
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world"); System.out.println(m == n); //true
System.out.println(m == u); //false
System.out.println(m == v); //false
System.out.println(u == v); //false
结论:
m和n是同一个对象
m,u,v都是不同的对象
m,u,v,n但都使用了同样的字符数组,并且用equal判断的话也会返回true
2.2、常量池实现及优化
保留字符串不能随意进行,但是如果有大量重复的字符串,占据了很大一部分堆,这时就很有效果了。保留字符串的表是保存在原生内存中的,它是一个大小固定的Hashtable。在java7u40之前的版本中,这个表默认有1009个桶,平均而言,在因为链接而出现冲突之前,预计可以保存500个字符串。在64位版本的java7u40及更新的版本中,默认大小为60013。
从java7开始,这个表(保留字符串表)大小可以在JVM启动时使用-XX:StringTableSize=N(默认1009或60013)。如果某个应用会保留大量字符串,就应该增加这个值。如果这个值是个素数,字符串保留表的效率最高。
intern()方法的性能是由表大小的调优程度所决定的。在《java性能权威指南》中有个示例:表7-3列出了在不同场景下创建和保留1千万个随机创建的字符串的总时间:
注意,如果字符串保留表的大小设置不当,性能损失会相当严重。一旦根据预期数据设置了该表的大小,性能会极大改善。
某个应用中已经分配的保留字符串个数(及其总大小),可以使用如下的jmap命令获得(这也需要JDK 7u6或者更新版本):
[ciadmin@2-103test_app ~]$ jmap -heap 26964
Attaching to process ID 26964, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11
...
Heap Usage:
PS Young Generation
Eden Space:
capacity = 64487424 (61.5MB)
used = 44652520 (42.583961486816406MB)
free = 19834904 (18.916038513183594MB)
69.24221379970147% used
From Space:
capacity = 1572864 (1.5MB)
used = 1048928 (1.000335693359375MB)
free = 523936 (0.499664306640625MB)
66.68904622395833% used
To Space:
capacity = 1572864 (1.5MB)
used = 0 (0.0MB)
free = 1572864 (1.5MB)
0.0% used
PS Old Generation
capacity = 456130560 (435.0MB)
used = 74748280 (71.28551483154297MB)
free = 381382280 (363.71448516845703MB)
16.387474673917925% used 29515 interned Strings occupying 3619152 bytes.
[ciadmin@2-103test_app ~]$
如果将字符串表设得特别大,其损失是非常小的:每个桶只需要4字节或8字节(取决于使用的是32位还是64位的JVM),所以比最优的情况多几千,只是一次性消耗一些原生内存(不是堆内存)。
另外,如果想看看字符串标的执行过程,可以使用-XX:+PrintStringTableStatistics参数,在JVM退出时,它会打印一个这样的列表:
这个例子有3035072个保留字符串(因为有1009个桶,每个桶平均有3008个字符串,1009*3008=3035072)。理想情况下,桶的平均大小应该是0或1.这个大小实际上不会为0,可能会小于0.5,但是因为技术时用的是整形运行,所以报告中会向下取整。(不理解)
2.3、常量池存放区域变化
总结:jdk1,6常量池放在方法区(也即是Perm空间),jdk1.7,jdk1.8常量池放在堆内存。所以导致string的intern方法因为以上变化在不同版本会有不同表现。
下面,我们通过测试程序来窥探字符串常量池在Java6,Java7两个不同版本底下的内存分配情况。
package com.dxz.metaspace; public class StringPoolTest {
public void testStringPoolWithLongString() {
long i = 0;
while (true) {
String longString = "This is a very long string, very very long string to test the gc behavior of the string constant pool"
+ i;
longString.intern();
i++;
}
} public static void main(String[] args) {
StringPoolTest stringPoolTest = new StringPoolTest();
stringPoolTest.testStringPoolWithLongString();
}
}
测试程序很简单,一个死循环,循环里面通过递增变量i制造唯一的字符串,然后用main函数启动程序。
Java 6
我们使用版本Jdk1.6.0_29来跑该程序,打开Java VisualVM监控,可以看到,Perm区不断发生GC,由此的出结论,虽然字符串常量池放在Perm空间,但当Perm空间接近满的时候,JVM会将字符串常量池中的无用字符串回收掉。
Java 7
下面,我们切换到Jdk1.7.0_67重跑该程序,可以看到Perm区内存分配曲线很平滑,没有出现内存分配的现象。
但在Heap空间,新的对象不断产生,然后不断触发GC
示例2
其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。我们可以通过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:
package com.dxz.jvm;
import java.util.ArrayList;
import java.util.List; public class StringOomMock {
static String base = "String";
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i=0;i< Integer.MAX_VALUE;i++){
String str = base + i;
list.add(str.intern());
}
}
}
这段程序以2的指数级不断的生成新的字符串,这样可以比较快速的消耗内存。我们通过 JDK 1.6、JDK 1.7 和 JDK 1.8 分别运行:
JDK 1.6 的运行结果:
JVM参数设置如下:
-XX:MaxPermSize=4M
-Xmx512M
-verbose -verbose:gc
JDK 1.7的运行结果:
-XX:MaxPermSize=4M
-Xmx42M
-verbose -verbose:gc
JDK 1.8的运行结果:
-XX:MaxMetaspaceSize=8M
-XX:PermSize=8m
-XX:MaxPermSize=8m
从上述结果可以看出,JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。(上面日志中的最后两行)因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。现在我们看看元空间到底是一个什么东西?
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
现在我们在 JDK 8下重新运行一下代码段 4,不过这次不再指定 PermSize 和 MaxPermSize。而是指定 MetaSpaceSize 和 MaxMetaSpaceSize的大小。输出结果如下:
从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。
JVM体系结构之七:持久代、元空间(Metaspace) 常量池==了解String类的intern()方法、常量池介绍、常量池从Perm-->Heap的更多相关文章
- 转:Java8内存模型—永久代(PermGen)和元空间(Metaspace)
一.JVM 内存模型 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 1.虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建.栈里面存着的是一种叫“栈 ...
- Java8内存模型—永久代(PermGen)和元空间(Metaspace)(转)
Java8内存模型—永久代(PermGen)和元空间(Metaspace) 查看原文点击传送门:http://www.cnblogs.com/paddix/p/5309550.html 提示:本文做了 ...
- (转)Java8内存模型—永久代(PermGen)和元空间(Metaspace)
背景:介绍java8中永久代到元空间的转变. Java8内存模型—永久代(PermGen)和元空间(Metaspace) 一.JVM 内存模型 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法 ...
- Java8内存模型—永久代(PermGen)和元空间(Metaspace)
一.JVM 内存模型 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 1.虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建.栈里面存着的是一种叫“栈 ...
- Java8内存结构—永久代(PermGen)和元空间(Metaspace)
本文转载 作者:liuxiaopeng 博客地址:https://www.cnblogs.com/paddix/p/5309550.html 一.JVM 内存结构 根据 JVM 规范,JVM 内存共分 ...
- JVM参数最佳实践:元空间的初始大小和最大大小
本文阅读时间大约4分钟. JVM加载类的时候,需要记录类的元数据,这些数据会保存在一个单独的内存区域内,在Java 7里,这个空间被称为永久代(Permgen),在Java 8里,使用元空间(Meta ...
- JVM体系结构之三:方法区之2(jdk1.6,jdk1.7,jdk1.8下的方法区变迁)
方法区 方法区存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.HotSpot中也称为永久代(Permanent Generation),(存储的是除了Java应用程序创建的对象之 ...
- JVM体系结构之三:方法区之1
一.简介 方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域.在方法区中,存储了每个类的信息(包括类的名称.方法信息.字段信息).静态变量.常量以及编译器编译后的代码等. 方法区( ...
- JDK方法区、元空间区别 & String.intern相关面试题
一.方法区.永久代.元空间 1.方法区.永久代 方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息.常量.静态变量.即时编译器编译后的代码等数据.方法区域又被称为"永久代& ...
随机推荐
- [微信开发] - 使用weixin4j进行二次开发
1. 服务器连接配置OK, 配置文件在classpath:weixin4j.properties中 # weixin4j-spring-demo### 使用weixin4j(岸思版)springboo ...
- MyBatis使用小案例
首先回顾一下MyBatis封装简化Dao层连接数据库操作的顺序. 首先MyBatis是一个引入的jar包,还有一些依赖包,可能用不到的jar包,一并引入就好了,再多引入一个Juntil.jar测试包( ...
- Bitmap 位图
转自: http://dongxicheng.org/structure/bitmap/ 1. 概述 位图(bitmap)是一种非常常用的结构,在索引,数据压缩等方面有广泛应用.本文介绍了位图的实现 ...
- URL存在http host头攻击漏洞-修复方案
URL存在http host头攻击漏洞-修复方案 spring boot使用注解的方式 -- 第一步:在自定义filter类上添加如下注释 package com.cmcc.hy.mobile.con ...
- 51Nod 1596 搬货物
http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1596 思路: 模拟二进制的进位. 这题很坑啊...用c++会超时,用c就 ...
- 在 php 中使用 strace、gdb、tcpdump 调试工具
转自:http://www.syyong.com/php/Using-strace-GDB-and-tcpdump-debugging-tools-in-PHP.html 在 php 中我们最常使用调 ...
- lua 删除table元素
tab1 = { key1 = "val1", key2 = "val2", "val3" } for k, v in pairs(tab1 ...
- [原][osgearth]osgearth本地(离线)数据源处理小结
参考:http://docs.osgearth.org/en/latest/data.html Processing Local Source Data If you have geospatial ...
- angular-cli 文档
Angular/angular-cli 原文来自:https://github.com/angular/angular-cli Angular/angular-cli 原文来自:https://git ...
- 微信分享签名Java代码实现
最近写了一个小微信签名功能,记录一下希望用到的朋友可以参考下. RestController @RequestMapping("/api/wx") public class Wei ...