开启String去重XX:+UseStringDeduplication的利与弊

开启String去重XX:+UseStringDeduplication的利与弊

原文在这里: 开启String去重XX:+UseStringDeduplication的利与弊

首先来看下由JDK开发组研究得出的一组有趣的统计数据:

1.java应用内存里面的字符串占比大概是25%。

2.java应用内存里面的字符串有13.5%是重复的。

3.字符串的平均长度是45。

由于存在重复字符串导致高达13.5%的内存被浪费了!你可以用HeapHero来分析看下你的应用中有多少内存是因为重复字符串被浪费掉的。

什么是重复字符串呢?

看下面的代码:

String string1 = new String("Hello World");
String string2 = new String("Hello World");

上面定义了两个字符串对象string1和string2,它们的内容是一样的,都是Hello World,但是string1和string2是两个不同的对象。string1.equals(String2)=true,但是string1==string2=false,string1和string2就是所谓的重复对象。
为啥有这么多的重复字符串?
应用中存在重复字符串的原因有很多种,下面我们来看下常见的两种情况:
1.开发者为每一个请求都创建新的字符串对象,而不是使用诸如public satatic final String 来重复利用他们,上面的例子可以用以下方式进行优化:

public static final String HELLO_WORLD = "Hello World";
String string1 = HELLO_WORLD;
String string2 = HELLO_WORLD;

 

2.假如你正在开发一个银行或者电商系统,数据库中的每一笔交易记录都需要存储货币名称(‘USD’, ‘EUR’, ‘INR’, ….),现在有一个用户登陆了系统然后查看历史的交易记录,因此你的应用需要从数据库中读取这个用户的所有交易记录。如果这个用户是在美国,因为每一笔交易都有货币,因此你的应用需要为每一笔交易都创建USD这个字符串,如果这个用户有成千上万比的交易,你的应用就需要为这一个用户在内存中创建成千上万个USD字符串。

与此类似,应用还需要多次从数据库中读取很多列(用户名、地址、状态、联系方式等等),这个过程中也会产生重复对象。你的应用通过JSON/XML跟外部进行交互的时候,会进行大量的字符串操作,也会产生大量的重复字符串。

JDK开发组很早就注意到这个问题了,因此后来也产生了很多解决方案,最新的解决方案就是使用XX:+UseStringDeduplication参数。
-XX:+UseStringDeduplication
花最小力气去掉重复字符串的办法就是使用XX:+UseStringDeduplication参数,当在JVM启动时传递了这个参数的时候,JVM在做GC的同时会做重复字符串消除。GC的时候,JVM会检查内存中的所有对象,然后识别出重复的字符串对象并消除之。

是不是意味着只要给JVM传递XX:+UseStringDeduplication就可以立马节省13.5%的内存?听上去很简单对吧?希望如此吧!但是还是有些地方需要注意的:

(1)只适用于G1收集器

JVM中有很多的GC收集器(串行、并行、CMS等等),XX:+UseStringDeduplication只能用在G1收集器上,如果你是使用的别的收集器需要切换到G1才可以。

(2)只适用于长期存活的对象

XX:+UseStringDeduplication主要是用来消除长时间存活的重复字符串对象,它不会对短期存活的对象做去重。如果字符串的生命周期很短,很可能还没来记得做去重就已经死亡了。这个文章(https://blog.gceasy.io/2018/07/17/disappointing-story-on-memory-optimization/)中的javaweb应用开启了string去重但是却对内存没有任何影响,但是,如果你的应用有大量的缓存(尤其是缓存的对象是长时间存活的对象),UseStringDeduplication这个参数就可能非常有用了!

(3)-XX:StringDeduplicationAgeThreshold

默认情况下,一个字符串对象经过3次GC以后还存活才会被列为去重的候选对象,可以用-XX:StringDeduplicationAgeThreshold来改变经历的GC的次数,比如:

-XX:StringDeduplicationAgeThreshold=6

(4)可能会增加GC停顿时间

因为是在GC的时候做的字符串去重,因此可能会增加GC的停顿时间。但是,如果有很高的去重率可能会抵消掉这部分影响,因为去重以后可以减轻GC的其他阶段的时间花费,同时还可以减少GC的频率(因为减少了堆的大小),可以用gceasy之类的工具来分析下GC停顿时间。

(5)只有底层的char[]会被替换掉
java.lang.String有2个成员变量:

private final char[] value
private int hash

-XX:+UseStringDeduplication并不会消除重复字符串的引用本身,它只会替换底层的char[],消除重复字符串对象只需要对value字段进行重新引用赋值即可, 差不多就是这个意思:aString.value = anotherString.value。

任何一个String对象在内存中最少占用24个字节,启用这个参数以后如果有很多短的重复字符串对象的话会大大的节省内存。

(6)仅能用于Java 8 update 20以后

只有从Java 8 update 20以后才开始支持这个参数,因此,如果你的JDK版本比较老的话就没法用这个功能了。

(7)-XX:+PrintStringDeduplicationStatistics查看去重信息

如果你想查看字符串去重的一些统计信息,比如说去重花了多长时间、多少重复字符串被去重、节省了多少内存等等,可以传递-XX:+PrintStringDeduplicationStatistics这个参数给JVM,在GC日志中就可以打印出这些信息来。

结论

如果你的应用使用的是G1收集器,并且JDK的版本大于 JDK 8 update 20,那么可以尝试开启-XX:+UseStringDeduplication,如果你的应用中存在大量长时间存活的对象,那结果肯定是很香。但是,如果想用在生产环境中还是需要测试一下才可以。
英文原文:https://blog.gceasy.io/2018/12/23/usestringdeduplication/

 

 

 

 

posted @ 2022-11-15 10:59  kelelipeng  阅读(243)  评论(0编辑  收藏  举报