JVM技术专题深入研究字符串常量池的相关知识体系「入门篇」
Posted 浩宇の天尚
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM技术专题深入研究字符串常量池的相关知识体系「入门篇」相关的知识,希望对你有一定的参考价值。
字符串冗余问题
从平均情况来看,应用程序中的String对象会消耗大量的内存。这里面有一部分是冗余的——同样的字符串会存在多个不同的实例(a != b, 但a.equals(b))。在实践中,有许多字符串会出于不同的原因造成冗余。
最初JDK提供了一个String.intern() 方法来解决字符串冗余的问题。这个方法的缺点在于你必须得去找出哪些字符串需要进行驻留(interned)。通常都需要具备冗余字符串查找功能的堆分析的工具才行,比如Youkit profiler。如果使用得当的话,字符串驻留会是一个非常有效的节省内存的工具——它让你可以重用整个字符串对象(每个字符串对象在底层char[]的基础上会增加24字节的额外开销)。
Java 7 update 6开始,每个String对象都有一个自己专属的私有char[] 。这样JVM才可以自动进行优化——既然底层的char[]没有暴露给外部的客户端的话,那么JVM就能去判断两个字符串的内容是否是一致的,进而将一个字符串底层的char[]替换成另一个字符串的底层char[]数组。
字符串去重这个特性就是用来做这个的,它在Java 8 update 20中被引入。下面是它的工作原理:
-
1.你得使用G1垃圾回收器并启用这一特性:
-XX:+UseG1GC -XX:+UseStringDeduplication
这一特性作为G1垃圾回收器的一个可选的步骤来实现的,如果你用的是别的回收器是无法使用这一特性的。 -
2.这个特性会在G1回收器的minor GC阶段中执行。根据我的观察来看,它是否会执行取决于有多少空闲的CPU周期。因此,你不要指望它会在一个处理本地数据的数据分析器中会被执行,也就是说,WEB服务器中倒是很可能会执行这个优化。
-
3.字符串去重会去查找那些未被处理的字符串,计算它们的hash值(如果它没在应用的代码中被计算过的话),然后再看是否有别的字符串的hash值和底层的char[]都是一样的。如果找到的话——它会用一个新字符串的char[]来替换掉现有的这个char[]。
-
4.字符串去重只会去处理那些历经数次GC仍然存活的那些字符串。这样能确保大多数的那些短生命周期的字符串不会被处理。字符串的这个最小的存活年龄可以通过 -XX:StringDeduplicationAgeThreshold=3 的JVM参数来指定(3是这个参数的默认值)。
下面是这个实现的一些重要的结论:
没错,如果你想享受字符串去重特性的这份免费午餐的话,你得使用G1回收器。
使用parellel GC的话是无法使用它的,而对那些对吞吐量要求比延迟时期高的应用而言,parellel GC应该是个更好的选择。
字符串去重是无法在一个已加载完的系统中运行的。要想知道它是否被执行了,可以通过 -XX:+PrintStringDeduplicationStatistics参数来运行JVM,并查看控制台的输出。
如果你希望节省内存的话,你可以在应用程序中将字符串进行驻留(interned)——那么放手去做吧,不要依赖于字符串去重的功能。
你需要时刻注意的是字符串去重是要处理你所有的字符串的(至少是大部分吧)——也就是说尽管你知道某个指定的字符串的内容是唯一的(比如说GUID),但JVM并不知道这些,它还是会尝试将这个字符串和其它的字符串进行匹配。这样的结果就是,字符串去重所产生的CPU开销既取决于堆中字符串的数量(将新的字符串和别的字符串进行比较),也取决于你在字符串去重的间隔中所创建的字符串的数量(这些字符串会和堆中的字符串进行比较)。
在一个拥有好几个G的堆的JVM上,可以通过-
XX:+PrintStringDeduplicationStatistics选项来看下这个特性所产生的影响究竟有多大。
另一方面,它基本是以一种非阻塞的方式来完成的,如果你的服务器有足够多的空闲CPU的话,那为什么不用呢?
最后,请记住,String.intern可以让你只针对你的应用程序中指定的某一部分已知会产生冗余的字符串。通常来说,它只需要比较一个较小的驻留字符串的池就可以了,也就是说你可以更高效地使用你的CPU。不仅如此,你还可以将整个字符串对象进行驻留,这样每个字符串你还多节省了24个字节。
这里是我用来试验这一特性的一个测试类。这三个测试都会一直运行到JVM抛出OOM为止,因此你得分别去单独地运行它们。
第一个测试会创建内容一样的字符串,如果你想知道当堆中字符串很多的时候,字符串去重会花掉多少时间的话,这个测试就变得非常有用了。尽量给第一个测试分配尽可能多的内存——它创建的字符串越多,优化的效果就越好。
第二三个测试会比较去重(第二个测试)及驻留(interning, 第三个测试)间的差别。你得用一个相同的Xmx设置来运行它们。在程序中我把这个常量设置成了Xmx256M,但是当然了,你可以分配得多点。然而,你会发现,和interning测试相比,去重测试会更早地挂掉。这是为什么?因为我们在这组测试中只有100个不同的字符串,因此对它们进行驻留就意味着你用到的内存就只是存储这些字符串所需要的空间。而字符串去重的话,会产生不同的字符串对象,它仅会共享底层的char[]数组。
/**
* String deduplication vs interning test
*/
public class StringDedupTest
private static final int MAX_EXPECTED_ITERS = 300;
private static final int FULL_ITER_SIZE = 100 * 1000;
//30M entries = 120M RAM (for 300 iters)
private static List<String> LIST = new ArrayList<>( MAX_EXPECTED_ITERS * FULL_ITER_SIZE );
public static void main(String[] args) throws InterruptedException
//24+24 bytes per String (24 String shallow, 24 char[])
//136M left for Strings
//Unique, dedup
//136M / 2.9M strings = 48 bytes (exactly String size)
//Non unique, dedup
//4.9M Strings, 100 char[]
//136M / 4.9M strings = 27.75 bytes (close to 24 bytes per String + small overhead
//Non unique, intern
//We use 120M (+small overhead for 100 strings) until very late, but can't extend ArrayList 3 times - we don't have 360M
/*
Run it with: -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics
Give as much Xmx as you can on your box. This test will show you how long does it take to
run a single deduplication and if it is run at all.
To test when deduplication is run, try changing a parameter of Thread.sleep or comment it out.
You may want to print garbage collection information using -XX:+PrintGCDetails -XX:+PrintGCTimestamps
*/
//Xmx256M - 29 iterations
fillUnique();
/*
This couple of tests compare string deduplication (first test) with string interning.
Both tests should be run with the identical Xmx setting. I have tuned the constants in the program
for Xmx256M, but any higher value is also good enough.
The point of this tests is to show that string deduplication still leaves you with distinct String
objects, each of those requiring 24 bytes. Interning, on the other hand, return you existing String
objects, so the only memory you spend is for the LIST object.
*/
//Xmx256M - 49 iterations (100 unique strings)
//fillNonUnique( false );
//Xmx256M - 299 iterations (100 unique strings)
//fillNonUnique( true );
private static void fillUnique() throws InterruptedException
int iters = 0;
final UniqueStringGenerator gen = new UniqueStringGenerator();
while ( true )
for ( int i = 0; i < FULL_ITER_SIZE; ++i )
LIST.add( gen.nextUnique() );
Thread.sleep( 300 );
System.out.println( "Iteration " + (iters++) + " finished" );
private static void fillNonUnique( final boolean intern ) throws InterruptedException
int iters = 0;
final UniqueStringGenerator gen = new UniqueStringGenerator();
while ( true )
for ( int i = 0; i < FULL_ITER_SIZE; ++i )
LIST.add( intern ? gen.nextNonUnique().intern() : gen.nextNonUnique() );
Thread.sleep( 300 );
System.out.println( "Iteration " + (iters++) + " finished" );
private static class UniqueStringGenerator
private char upper = 0;
private char lower = 0;
public String nextUnique()
final String res = String.valueOf( upper ) + lower;
if ( lower < Character.MAX_VALUE )
lower++;
else
upper++;
lower = 0;
return res;
public String nextNonUnique()
final String res = "a" + lower;
if ( lower < 100 )
lower++;
else
lower = 0;
return res;
Java 8 update 20中添加了字符串去重的特性。它是G1垃圾回收器的一部分,因此你必须使用G1回收器才能启用它:
-XX:+UseG1GC -XX:+UseStringDeduplication
字符串去重是G1的一个可选的阶段。它取决于当前的系统负载。
字符串去重会查询内容相同那些字符串,并将它们底层存储字符的char[]数组进行统一。使用这一特性你不需要写任何代码,不过这意味着最后你得到的是不同的字符串对象,每个对象会占用24个字节。有的时候显式地调用String.intern进行驻留还是有必要的。
字符串去重不会对年轻的字符串进行处理。字符串处理的最小年龄是通过-XX:StringDeduplicationAgeThreshold=3的JVM参数来进行管理的(3是这个参数的默认值)
以上是关于JVM技术专题深入研究字符串常量池的相关知识体系「入门篇」的主要内容,如果未能解决你的问题,请参考以下文章