β

G1垃圾回收器中的字符串去重(Java 8 Update 20)

ImportNew 53 阅读
原文出处: 鸟窝

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

起初,JDK提供String.intern()方法处理字符串重复的问题。该方法的缺点是你需要找出哪些字符串需要驻留(interned)。这通常需要一个具备重复字符串查找功能的堆分析工具,比如 YourKit profiler 。尽管如此,如果使用恰当,字符串驻留会是一个强大的节省内存的工具-它允许你重用整个String对象(每个对象会在底层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. 字符串去重只会处理那些经历过几次垃圾收集仍然存活的字符串,这确保多数生命周期很短的字符串不会被处理。字符串的这个最小存活年龄是通过JVM参数-XX:StringDeduplicationAgeThreshole=3管理的(3是该参数的默认值)。

下面是关于这个实现的一些重要结论:

特性测试

这是我用来试验这一特性的一个测试类,这3个测试都需要运行到JVM抛出OOM,所以需要单独运行。
第一个测试会创建内容不同的字符串,如果你想模拟当堆中有大量字符串时,字符串去重花费的时间,那这个测试是非常有用的。尽量为第一个测试分配尽可能多的内存-创建的字符串越多,去重效果越好。
第二个和第三个测试用于比较字符串去重(第二个测试)和驻留(第三个测试)间的差别。你需要使用相同的(identical)Xmx设置来运行它们。在程序中,我把这个常量设为Xmx256M,你可以多分配些。然而,你会发现在迭代几次后去重测试将先失败,然后是驻留测试。这是为什么?因为,在这些测试中我们只有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;
        }
    }
}

总结

参考

作者:ImportNew
原文地址:G1垃圾回收器中的字符串去重(Java 8 Update 20), 感谢原作者分享。