Java是什么?
Java历史
Java语言特点
C++ VS Java比较
Java工厂设计模式
Java抽象工厂模式
Java单例模式
Java建造者(Builder)模式
Java原型模式
Java适配器模式
Java桥接模式
Java获取网络文件大小
Java套接字到单一的客户端
Java连接套接字
Java URL部分
Java URL连接日期
Java下载网页
Java主机指定IP地址
Java确定本地IP地址
Java检查端口占用
Java查找代理服务器设置
Java创建Socket
Java线程实例
Java检查线程活着
Java如何检查一个线程停止或没有?
Java解决死锁实例
Java如何获取正在运行的线程的优先级?
Java如何监视线程的状态?
Java获取线程名称
Java线程生产者消费者问题
Java如何设置线程的优先级?
Java如何停止线程一会儿?
Java如何暂停线程?
Java获取线程ID
Java如何检查线程的优先级?
Java显示所有正在运行的线程?
Java显示线程状态
Java中断一个线程
Java Applet实例
Java创建Applet
Java使用Applet创建横幅
Java使用Applet显示时钟?
Java在一个Applet创建不同形状
Java如何使用Applet填充形状的颜色?
Java使用Applet跳转到一个链接
Java在Applet创建事件监听器
Java使用Applet显示图像
Java使用Applet在新窗口中打开链接
Java使用Applet播放声音?
Java使用Applet读取文件
Java使用Applet写入文件
Java中Swing应用程序applet
Java简单的图形用户界面-GUI
Java以不同的字体显示文本
Java使用GUI画一条线
Java创建框架-frame
Java使用GUI显示多边形
Java在矩形中显示文本
Java GUI显示不同形状
Java如何绘制GUI实心矩形?
Java创建GUI透明光标
Java检查GUI平滑处理状态
Java在框架中显示颜色
Java GUI显示饼图
Java使用图形用户界面绘制文本
Java编辑表-table
Java 使用prepared语句
Java使用保存点和回滚
Java同时执行数据库多个SQL命令
Java使用行方法
Java使用列方法
Java正则表达式实例
Java将字符串分割
Java搜索重复单词
Java查找出现的单词
Java最后一个词的索引
Java模式匹配
Java删除空格
Java匹配电话号码
Java计数组词
Java搜索词组
Java拆分正则表达式
Java替换第一个出现字符串
Java检查日期格式
Java验证电子邮件地址格式
Java替换所有匹配字符串
Java使每个单词的第一个字母大写
从XML创建SqlSessionFactory实例
不使用XML来创建SqlSessionFactory
从SqlSessionFactory获取SqlSession
映射SQL语句
作用域和生命周期
Mapper XML配置
properties元素
Settings元素
typeAliases 元素
typeHandlers元素
理解CacheLine与写出更好的JAVA
Java核心技术点之动态代理
更好的使用JAVA线程池
理解Java中字符流与字节流的区别
深入分析Java方法反射的实现原理
关于Java面试,你应该准备这些知识点
Java内存模型
2017年你不能错过的Java类库
Leakcanary Square的一款Android/Java内存泄漏检测工具
Java Synchronised机制
Java核心技术点之注解
JVM(8):JVM知识点总览-高级Java工程师面试必备
JVM(3):Java GC算法 垃圾收集器
JVM(1):Java 类的加载机制
解决ActiveMQ中,Java与C++交互中文乱码问题
关于Java Collections的几个常见问题
Java I/O 总结
JVM源码分析之Java对象的创建过程
JVM源码分析之Java类的加载过程
Java GC的那些事(下)
Java GC的那些事(上)
java对象头的HotSpot实现分析
面试的角度诠释Java工程师(一)
面试的角度诠释Java工程师(二)
框架开发之Java注解的妙用
谈谈Java反射机制
Java并发:volatile内存可见性和指令重排
死磕Java并发:Java内存模型之happens-before
死磕Java并发:深入分析volatile的实现原理
死磕Java并发:深入分析synchronized的实现原理
Java 10 可能对 Lambda 表达式进行升级
G1垃圾回收器中的字符串去重(Java 8 Update 20)
Java RESTful框架的性能比较
理解RxJava的线程模型
继续了解Java的纤程库 – Quasar
Java中的纤程库 – Quasar
Java豆瓣电影爬虫——抓取电影详情和电影短评数据
Java集合框架源码剖析:LinkedHashSet 和 LinkedHashMap
Java Lambda表达式初探
Java中的陷阱题
Java 9的这一基本功能,你可能从未听过
关于Java并发编程的总结和思考
几种简单的负载均衡算法及其Java代码实现
JAVA虚拟机关闭钩子(Shutdown Hook)
Java 脚本化编程指南
Java Scripting API 使用示例
Java 8 的 Nashorn 脚本引擎教程
如何开始使用 Java 机器学习
CognitiveJ —— Java 的图像分析库
Java 性能优化的五大技巧
Java 解惑:Comparable 和 Comparator 的区别
Google Java编程风格指南
java NIO详解
Java 异常处理的误区和经验总结
Java语法糖(4):内部类
Java语法糖(3):泛型
Java语法糖(2):自动装箱和自动拆箱
Java消息队列任务的平滑关闭
Java语法糖(1):可变长度参数以及foreach循环原理
2016最流行的Java EE服务器
自己写一个java.lang.reflect.Proxy代理的实现
java 如何在pdf中生成表格
如何防止单例模式被JAVA反射攻击
java虚拟机 jvm 局部变量表实战
聊聊并发-Java中的Copy-On-Write容器
java.lang.Instrument 代理Agent使用
Java开发者需要了解的移动开发编程语言
13个不容错过的Java项目
2016年7款最佳 Java 框架推荐
Java 开发者值得关注的 11 个技术博客
Redmonk发布Java框架流行度调研结果
Java 8开发的4大顶级技巧
GitHub漫游指南:10个值得你关注的Java项目
除了Guava,Java开发者还值得了解的5个谷歌类库
Java中创建对象的5种不同方法
Java性能优化全攻略
奇怪的Java题:为什么1000 == 1000返回为False,而100 == 100会返回为True?
11个最值得Java开发者收藏的网站
Java的常见误区与细节
对Java意义重大的7个性能指标
Java调优经验谈
关于Java并发编程的总结和思考
HDFS Federation设计动机与基本原理
《Effective STL》学习笔记(第三部分)
《Effective STL》学习笔记(第二部分)
《Effective STL》学习笔记(第一部分)
数据结构之位图
Thrift使用指南
Cassandra概要介绍
Cassandra部署与安装
Cassandra客户端
Cassandra数据模型
Cassandra中的各种策略
数据结构之树状数组
数据结构之伸展树
数据结构之后缀数组
数据结构之堆
浅析MRv1与MRv2的API兼容性
Apache Tez最新进展
运行在YARN上的计算框架
从传统操作系统角度理解Hadoop YARN

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

于2017-05-10由小牛君创建

分享到:


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

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

  • 没错,如果你想享受字符串去重特性这份免费午餐的话,你需要使用G1收集器。你不能使用并行GC,通常对于追求高吞吐量胜于低延迟的应用这可能是更好的选择。
  • 字符串去重无法在一个已加载完的系统中运行。为了检验它是否执行过,可以使用-XX:+PrintStringDeduplicationStatistics参数运行JVM,并观察控制台输出。
  • 如果需要节省内存,并且你可以在应用中驻留字符串-就这样做,不要依赖字符串去重的功能。你需要时刻注意的是字符串去重会处理所有或至少大部分字符串-也就是说尽管你知道某个给定的字符串内容是唯一的,比如GUID,但JVM不知道这些,它仍会尝试将这个字符串和其它字符串进行匹配。结果,字符串去重产生的CPU开销既取决于堆中字符串的数量(新的字符串会与它们中的一些进行比较),也取决于你在字符串去重期间创建的字符串数量(这些字符串需要和堆中的字符串比较)。在拥有好几个G的堆上,可以通过-XX:+PrintStringDeduplicationStatistics JVM选项检查这个特性的影响。
  • 另一方面, 字符串去重基本是以非阻塞的方式完成的,如果你的服务器有足够多的空闲CPU,那为什么不用呢?
  • 最后,请记住String.intern允许你只针对应用程序中那些已知的会产冗余的字符串进行驻留,通常它只需要跟一个很小的字符串驻留池比较即可,这样能更有效的利用CPU。此外,你可以驻留整个String对象,这样每个字符串可以额外节省24字节。

特性测试

这是我用来试验这一特性的一个测试类,这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;
        }
    }
}

总结

  • 字符串去重是Java 8 update 20添加的新特性。它是G1垃圾回收器的一部分,因此你必须使用G1回收器才能启用它:-XX:+UseG1GC -XX:+UseStringDeduplication。
  • 字符串去重是G1的一个可选阶段,它取决于当前系统的负载。
  • 字符串去重会查询内容相同的字符串,并统一底层存储字符的char[]数组。使用此特性你不需要编写任何代码,不过这意味着你最后得到的是不同的String对象,每个对象占用24字节。有时候,显式的调用String.intern方法进行字符串驻留还是有必要的。
  • 字符串去重不会处理太年轻的字符串。处理字符串的最小年龄是通过JVM参数:-XX:StringDeduplicationAgeThreshold=3来管理的(3是这个参数的默认值)。

参考