写点什么

☕【JVM 技术探索】字符串常量池之 G1 回收期的驻留机制

发布于: 2021 年 06 月 06 日
☕【JVM技术探索】字符串常量池之G1回收期的驻留机制

字符串冗余问题

从平均情况来看,应用程序中的 String 对象会消耗大量的内存。这里面有一部分是冗余的——同样的字符串会存在多个不同的实例(a != b, 但 a.equals(b))。在实践中,有许多字符串会出于不同的原因造成冗余。

字符串驻留技术

最初 JDK 提供了一个 String.intern() 方法来解决字符串冗余的问题。这个方法的缺点在于你必须得去找出哪些字符串需要进行驻留(interned)。通常都需要具备冗余字符串查找功能的堆分析的工具才行,比如 Youkit profiler。如果使用得当的话,字符串驻留会是一个非常有效的节省内存的工具——它让你可以重用整个字符串对象(每个字符串对象在底层 char[]的基础上会增加 24 字节的额外开销)。

Java7 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 个字节。


第一个测试会创建内容一样的字符串,如果你想知道当堆中字符串很多的时候,字符串去重会花掉多少时间的话,这个测试就变得非常有用了。尽量给第一个测试分配尽可能多的内存——它创建的字符串越多,优化的效果就越好。


第二三个测试会比较去重(第二个测试)及驻留(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; } }}
复制代码


字符串去重是 G1 的一个可选的阶段。它取决于当前的系统负载。

发布于: 2021 年 06 月 06 日阅读数: 11
用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
☕【JVM技术探索】字符串常量池之G1回收期的驻留机制