Java 8的字符串重复数据删除功能
由于Java中的String
(与其他语言一样)会消耗大量内存,因为每个字符都消耗两个字节,所以Java 8引入了一个名为String Deduplication的新特性,它利用char数组在字符串和final内部这一事实,所以JVM可能会混淆它们。
到目前为止,我已经阅读过这个例子,但由于我不是专业的java编码人员,所以我很难理解这个概念。
这就是它所说的,
已经考虑了字符串复制的各种策略,但是现在实现的策略遵循以下方法:无论何时垃圾收集器访问String对象时,都会注意到char数组。 它取得它们的散列值并将其与一个对数组的弱引用一起存储。 只要它发现另一个具有相同散列码的字符串,它就会将它们按char字符进行比较。 如果它们匹配,则会修改一个字符串并指向第二个字符串的char数组。 然后第一个字符数组不再被引用,可以被垃圾收集。
这整个过程当然会带来一些开销,但受到严格限制的控制。 例如,如果一个字符串没有被发现重复一段时间,它将不再被检查。
我的第一个问题,
关于这个话题还缺乏资源,因为它最近在Java 8 update 20中添加了,这里有没有人可以分享一些实际的例子来说明它如何帮助减少String
在Java中消耗的内存?
编辑:
上面的链接说,
只要它发现另一个具有相同散列码的字符串,它就会将它们按char字符进行比较
我的第二个问题,
如果两个哈希代码String
是相同的,则Strings
已经是相同的,那么为什么对它们进行比较char
通过char
一旦发现这两个String
具有相同的哈希码?
想象一下,你有一个电话簿,其中包含人,其中有一个String firstName
和一个String lastName
。 而在你的电话簿中,恰好有100,000人拥有相同的firstName = "John"
。
因为您从数据库或文件中获取数据,所以这些字符串不会被拦截,因此您的JVM内存包含char数组{'J', 'o', 'h', 'n'}
10万次,每个约翰字符串一个。 这些阵列中的每一个需要20个字节的内存,所以这些100k的Johns占用2MB的内存。
通过重复数据删除,JVM会意识到“John”被重复多次,并使所有John字符串指向相同的底层char数组,从而将内存使用量从2MB减少到20个字节。
你可以在JEP中找到更详细的解释。 尤其是:
许多大规模的Java应用程序目前都是内存瓶颈。 测量表明,在这些类型的应用程序中设置的Java堆实时数据的大约25%被String对象占用。 此外,大约一半的String对象是重复的,其中重复的意思是string1.equals(string2)
是真的。 在堆上存在重复的String对象本质上只是浪费内存。
[...]
实际的预期收益最终会降低10%左右。 请注意,该数字是基于广泛应用的计算平均值。 特定应用程序的堆栈缩减可能会在上下文中显着变化。
@assylias答案basiclly告诉你它是如何工作的,是非常好的答案。 我已经使用字符串重复数据消除测试了一个生产应用程序并获得了一些结果 网络应用大量使用Strings,所以我认为优势非常明显。
要启用字符串重复数据消除,您必须添加这些JVM参数(至少需要Java 8u20):
-XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics
最后一个是可选的,但名称表示它显示了字符串重复数据删除统计信息。 这是我的:
[GC concurrent-string-deduplication, 2893.3K->2672.0B(2890.7K), avg 97.3%, 0.0175148 secs]
[Last Exec: 0.0175148 secs, Idle: 3.2029081 secs, Blocked: 0/0.0000000 secs]
[Inspected: 96613]
[Skipped: 0( 0.0%)]
[Hashed: 96598(100.0%)]
[Known: 2( 0.0%)]
[New: 96611(100.0%) 2893.3K]
[Deduplicated: 96536( 99.9%) 2890.7K( 99.9%)]
[Young: 0( 0.0%) 0.0B( 0.0%)]
[Old: 96536(100.0%) 2890.7K(100.0%)]
[Total Exec: 452/7.6109490 secs, Idle: 452/776.3032184 secs, Blocked: 11/0.0258406 secs]
[Inspected: 27108398]
[Skipped: 0( 0.0%)]
[Hashed: 26828486( 99.0%)]
[Known: 19025( 0.1%)]
[New: 27089373( 99.9%) 823.9M]
[Deduplicated: 26853964( 99.1%) 801.6M( 97.3%)]
[Young: 4732( 0.0%) 171.3K( 0.0%)]
[Old: 26849232(100.0%) 801.4M(100.0%)]
[Table]
[Memory Usage: 2834.7K]
[Size: 65536, Min: 1024, Max: 16777216]
[Entries: 98687, Load: 150.6%, Cached: 415, Added: 252375, Removed: 153688]
[Resize Count: 6, Shrink Threshold: 43690(66.7%), Grow Threshold: 131072(200.0%)]
[Rehash Count: 0, Rehash Threshold: 120, Hash Seed: 0x0]
[Age Threshold: 3]
[Queue]
[Dropped: 0]
这些是运行该应用10分钟后的结果。 正如你所看到的,字符串重复数据删除被执行了452次,并被“重复数据删除”了801.6 MB字符串。 字符串重复数据删除检查了27 000 000个字符串。 当我将Java 7的内存消耗与带有G1 GC的标准并行GC与Java 8u20进行比较并启用了字符串重复数据删除时,堆大约下降了50% :
Java 7并行GC
带字符串重复数据消除的Java 8 G1 GC
既然你的第一个问题已经得到解答,我会回答你的第二个问题。
String
对象必须逐个字符地进行比较,因为虽然相等的Object
意味着相等的散列,但反过来并不一定是真的。
正如霍尔格在他的评论中所说的那样,这代表了哈希碰撞。
hashcode()
方法的适用规范如下所示:
如果两个对象根据equals(Object)
方法equals(Object)
,则对这两个对象中的每个对象调用hashCode
方法必须产生相同的整数结果。
根据equals(java.lang.Object)
方法,如果两个对象不相等,则不要求对两个对象中的每一个调用hashCode
方法都必须生成不同的整数结果。 ...
这意味着为了让他们保证平等,每个角色的比较是必要的,以便他们确认两个对象的平等。 他们从比较hashCode
开始,而不是使用equals
因为它们使用哈希表作为引用,这会提高性能。