`
deepinmind
  • 浏览: 446098 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
1dc14e59-7bdf-33ab-841a-02d087aed982
Java函数式编程
浏览量:40984
社区版块
存档分类
最新评论

JVM中的字符串去重

jvm 
阅读更多
本文将为你简单介绍一下Java 8 update 20中引入的字符串去重的特性。

从平均情况来看,应用程序中的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是这个参数的默认值)。

  • 原创文章转载请注明出处:Java译站


    英文原文链接
    1
    1
    分享到:
    评论

    相关推荐

      第4节: 揭秘JVM字符串常量池和Java堆-01

      第4节: 揭秘JVM字符串常量池和Java堆-01第4节: 揭秘JVM字符串常量池和Java堆-01第4节: 揭秘JVM字符串常量池和Java堆-01第4节: 揭秘JVM字符串常量池和Java堆-01第4节: 揭秘JVM字符串常量池和Java堆-01第4节: ...

      动态编译字符串成java,并且添加class到jvm

      动态编译字符串成java,并且添加class到jvm

      jvm如何处理长字符串

      jvm如何处理长字符串?java的classs文件中,constant_utf8_info的长度是u2,也就是说,一个字符串最长是65535个字节,但是,在本机做测试,超过这个长度的字符串也是允许的,原因是什么?

      测试JVM字符串长度的testcase

      用于测试Java字符串长度的testcase,-jar文件,可以通过设置JVM的内存设置 VM argument(eclipse中方便设置)来考察java string可以接受的长度 总体评价来看,java字符串可以任意长,除非字长超出JVM的内存限制,...

      计算机后端-Java-Java核心基础-第21章 常用类 10. JVM中涉及字符串的内存结构.avi

      计算机后端-Java-Java核心基础-第21章 常用类 10. JVM中涉及字符串的内存结构.avi

      Java对象创建方式及JVM对字符串处理

       在讲Jvm对字符串的处理之前,我们先来讲一下,在Java中,常见的5种创建对象的方式:  1)通过关键字new调用构造器创建Java对象,eg:String str = new String("hello");  2)通过Class对象的newInstance()...

      字符数组的存储方式 字符串常量池.docx

      字符串在java程序中被大量使用,为了避免每次都创建相同的字符串对象及内存分配,JVM内部对字符串对象的创建做了一定的优化,在Permanent Generation中专门有一块区域用来存储字符串常量池(一组指针指向Heap中的...

      JVM系列之String.intern的性能解析

      String对象有个特殊的...而G1垃圾回收器的字符串去重的功能其实和String.intern有点不一样,G1是让两个字符串的底层指向同一个byte[]数组。 有图为证: 上图中的String1和String2指向的是同一个byte[]数组。 Strin

      深入解析JVM之内存结构及字符串常量池(推荐)

      Java作为一种平台无关性的语言,其主要依靠于Java虚拟机——JVM,接下来通过本文给大家介绍JVM之内存结构及字符串常量池的相关知识,需要的朋友可以参考下

      java 查看JVM中所有的线程的活动状况

      java 查看JVM中所有的线程的活动状况 java 查看JVM中所有的线程的活动状况

      jvm字符转码

      字符转码 jvm字符集

      图解JVM的内存结构及字符串常量池方法详解.docx

      类加载 、 类文件结构 、 垃圾回收 、 执行 引擎 、 性能调优 、 监控 等等这些知识,但所有的功能都是围绕着 内存结构 展开的,因为我们编译后的代码信息在运行过程中都是存在于JVM自身的内存区域中的...

      对象在jvm中的存储情况

      java对象在jvm中的存储情况 jvm

      Java中关于字符串的若干问题分析.pdf

      分析,作者在多年的Java程序设计教学工作中,就字符串不同 的初始化方法对程序的影响、相等比较以及 String 类与 Strin gBuffer 类的区别三个方面做了系统分析,期望能够对 Java 歇 息提供一些参考和借鉴。

      JVM中文指令手册.pdf

      这个是jvm指令手册,可以通过该手册查找理解字节码程序,超详细,超好用!!

      string-split:在 Clojure 中拆分字符串的各种方法

      在 Clojure 中拆分字符串 该项目演示了在 Clojure 中拆分字符串的各种方法。 选择特定变体时,不要忘记检查输入要求: 一些变体适用于字符集合并支持动态处理和无限集合,一些仅适用于已加载到内存中的字符串。 ...

      Java中的字符串常量池详细介绍

      主要介绍了Java中的字符串常量池详细介绍,JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池,需要的朋友可以参考下

      jvm笔记

      字面常量包括了整数、长整数、字符串等类型,而符号引用则包括了方法的描述、字段的描述等信息。在 JVM 中,方法的描述包括了方法的名称、返回类型、参数列表等信息,而字段的描述则包括了字段的名称、类型、访问...

      String_字符串.html

      String对象是不可变的。JVM对其做了一个优化,在内存中开辟了一段区域作为字符串常量池。通过"字面量"形式创建的字符串对象都会缓存并重用。

    Global site tag (gtag.js) - Google Analytics