【问题标题】:String Deduplication feature of Java 8Java 8 的字符串重复数据删除功能
【发布时间】:2015-03-13 00:09:54
【问题描述】:

由于 Java 中的 String(与其他语言一样)由于每个字符占用两个字节而消耗大量内存,因此 Java 8 引入了一个名为字符串重复数据删除的新功能它利用了 char 数组在字符串和 final 的内部这一事实,因此 JVM 可以处理它们。

到目前为止,我已经阅读了this example,但由于我不是专业的 Java 编码器,所以我很难理解这个概念。

这就是它所说的,

已经考虑了字符串复制的各种策略,但是 现在实施的方法遵循以下方法:每当 垃圾收集器访问 String 对象,它记下 char 数组。它获取它们的哈希值并将其与弱 对数组的引用。一旦它找到另一个字符串 相同的哈希码将它们逐个字符地进行比较。如果它们匹配为 好吧,一个 String 将被修改并指向 char 数组 第二个字符串。然后不再引用第一个 char 数组 不再存在,可以被垃圾收集。

这整个过程当然会带来一些开销,但是是可控的 通过严格的限制。例如,如果一个字符串没有被发现有 重复一段时间就不再检查了。

我的第一个问题,

由于最近在 Java 8 更新 20 中添加了该主题,因此仍然缺乏资源,这里有人可以分享一些实际示例,说明它如何帮助减少 Java 中 String 消耗的内存吗?

编辑:

上面的链接说,

一旦找到另一个具有相同哈希码的字符串 逐个字符比较它们

我的第二个问题,

如果两个String的哈希码相同,那么Strings已经相同,那为什么要在发现两个String的哈希码相同时,用char比较char呢?

【问题讨论】:

  • 你听说过“哈希冲突”吗?只有2³² == 4294967296 不同的哈希码,但65536²¹⁴⁷⁴⁸³⁶⁴⁸ == practically infinite 可能有不同的Strings。换句话说,具有相同的哈希码保证String 相等。你必须检查一下。恰恰相反,具有不同的哈希码意味着Strings 不相等。
  • 我没有链接,因为很容易找到:一个char是一个16位的值,所以它允许2¹⁶ == 65536组合。 String 是一个长度为 int 的序列,因此它最多可以有 2³¹ 个字符(2³¹ 不是 2³²,因为 int 是用 Java 签名的,但 String 的大小是正数) 所以String 的最大长度是2³¹ == 2147483648(理论上,实际的限制要小一些)。所以 String 最多可以组合 2147483648 个字符,这些字符可以有 65536 种可能的组合,这使得 65536²¹⁴⁷⁴⁸³⁶⁴⁸ 组合(实际上有点大,因为 String 也可能更短)
  • @mbomb007:当有m 不同的数字允许mⁿ 组合时,就像有一个带有n 数字位置的数字一样,例如从000999 的十进制数字允许10³ 组合。对于String,在2147483648 数字位置有65536 不同的“数字”(又名chars),所以它是65536²¹⁴⁷⁴⁸³⁶⁴⁸。它只是“稍微”多一点,因为\0 和“end-of-String”在 Java 中是不同的。这并不重要,因为它太大了,无法想象。
  • 它应该等于 (2¹⁶)^(∑ n=0_31(2^n)) 如果你包含一个可以更短的String。我正是这个意思。这实际上并没有更多。
  • 相等的哈希码并不意味着相等的字符串。见stackoverflow.com/questions/27581/…

标签: java string java-8


【解决方案1】:

@assylias 的回答基本上会告诉你它是如何工作的,并且是非常好的答案。我已经使用字符串重复数据删除测试了一个生产应用程序并得到了一些结果。该网络应用程序大量使用字符串,所以我认为优势非常明显。

要启用字符串重复数据删除,您必须添加这些 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 与标准并行 GC 的内存消耗与 Java 8u20 与 G1 GC 进行比较并启用字符串重复数据删除时,堆下降了大约 50%

Java 7 并行 GC

带有重复数据删除的 Java 8 G1 GC

【讨论】:

  • 感谢您的出色回答。但是你能告诉我你用什么工具来测量内存消耗以及如何做的吗?任何指向 oracle/java 网站的详细说明都会非常有帮助。我想为我的网络应用程序做这样的分析。在此先感谢:)
  • 图表来自 NetBeans IDE - 来自内置分析器。查看 Netbeans 网站和 Google 上的教程。或者,您可以从 jVisualVM 获得相同的图表。
  • @RobertNiestroj,根据这篇文章cubrid.org/blog/dev-platform/… 我们不应该/不建议使用 G1GC。那么我们如何解决这个问题呢?
  • 什么问题?如果你不能使用 G1GC,那么你就不能使用 String Deduplication。没有解决方法。
  • @Reddy,文章指出不应该考虑 G1,因为它太新了。这是 JDK7 中的“新官方”。我不确定某些东西需要有多“新”才能变得太新,但 JDK7 于 2011 年问世。
【解决方案2】:

假设您有一个电话簿,其中包含人员,其中有 String firstNameString lastName。碰巧在您的电话簿中,有 100,000 人拥有相同的firstName = "John"

因为您从数据库或文件中获取数据,所以这些字符串没有被保留,因此您的 JVM 内存包含 char 数组 {'J', 'o', 'h', 'n'} 10 万次,每个 John 字符串一个。例如,这些数组中的每一个都占用 20 字节的内存,因此这 100k Johns 占用了 2 MB 的内存。

通过重复数据删除,JVM 将意识到“John”被重复多次,并使所有这些 John 字符串指向同一个底层 char 数组,从而将内存使用量从 2MB 减少到 20 字节。

您可以在JEP 中找到更详细的说明。特别是:

目前,许多大型 Java 应用程序都存在内存瓶颈。测量表明,在这些类型的应用程序中,大约 25% 的 Java 堆实时数据集被 String 对象消耗。此外,这些 String 对象中大约有一半是重复的,其中重复意味着 string1.equals(string2) 为真。在堆上拥有重复的 String 对象本质上只是浪费内存。

[...]

实际预期收益最终会减少大约 10% 的堆。请注意,此数字是根据广泛的应用计算得出的平均值。特定应用程序的堆减少量可能上下变化很大。

【讨论】:

  • @Joe 您将不得不询问 JVM 设计人员 - 字符串实习已经存在了很长时间,我怀疑随着 JVM/垃圾收集器的性能变得更好以及每个 CPU 的数量设备增加,他们可以改进过去会引入过多开销的事情。
  • 在旧版本中,String 可以使用int 偏移量和长度来引用数组中的范围。在这种情况下,重复数据删除会复杂得多,但另一方面,String.substring 的结果不需要它,因为这些子字符串引用了原始数组。这在 Java 7 中发生了变化,提高了对重复数据删除功能的需求。
  • 在字符串中查找子字符串要慢得多 (O(n^2) ?),而查找两个字符串是否相等是 O(n) 最坏情况和 O(1) 当两个字符串有不同的(缓存的)哈希码,即大多数时候它是一个简单的 int 比较。
  • @Joe 我添加了一个可能更好地回答您的问题的链接。
  • 您的意思是:没有两个字符串...?我读起来很不一样。
【解决方案3】:

既然你的第一个问题已经回答了,那我来回答你的第二个问题。

String 对象必须逐个字符进行比较,因为虽然相等的 Objects 意味着相等的哈希值,但反之不一定为真。

正如Holger 在他的comment 中所说,这代表了哈希冲突。

hashcode()方法的适用规范如下:

  • 如果两个对象根据equals(Object) 方法相等,那么对两个对象中的每一个调用hashCode 方法必须产生相同的整数结果。

  • 如果根据equals(java.lang.Object) 方法,如果两个对象不相等,则不需要对这两个对象中的每一个调用hashCode 方法必须产生不同的整数结果。 ...

这意味着为了让它们保证相等,每个字符的比较是必要的,以便它们确认两个对象的相等。他们首先比较 hashCodes 而不是使用 equals,因为他们使用哈希表作为引用,这样可以提高性能。

【讨论】:

  • 这没有回答原始(主要)问题。
  • 实际上我没有编辑主要问题...它总是在那里,因为您可以看到其他答案。
  • 他没有回答你的第二个问题,所以我添加了一些信息,希望对阅读它的人有所帮助和信息。
  • 我都阅读了,但我已经看到了接受的答案,所以我只想提供新信息。
  • 谢谢...您能否编辑您的答案并添加一个声明,即这是第二个问题的答案...我认为这对未来的读者会有帮助。 :) PS,+1
【解决方案4】:

他们描述的策略是在可能的多个equal 字符串中简单地重用一个字符串的内部字符数组。如果它们相等,则每个 String 都不需要拥有自己的副本。

为了更快地确定两个字符串是否相等,首先使用哈希码,因为它是一种快速确定字符串可能是否相等的方法。因此他们的声明:

一旦找到另一个具有相同哈希码的字符串,它就会逐个字符地比较它们

这是为了在使用哈希码确定可能相等后,对相等进行一定(但速度较慢)比较。

最后,相等的字符串将共享一个底层的 char 数组。

Java 已经使用String.intern() 很长时间了,它或多或少地做同样的事情(即通过重复删除相等的字符串来节省内存)。它的新颖之处在于它发生在垃圾回收期间,并且可以外部控制。

【讨论】:

  • 您刚刚复制了该示例链接中的内容。
  • @Joe 我在试图解释为什么哈希码与所有这些相关时引用了他们的部分声明。编辑答案以使其更加明显。
  • 如果您引用其他文档的部分内容,您应该使用块引用样式并提供某种形式的署名。
猜你喜欢
  • 2015-12-27
  • 2016-11-03
  • 1970-01-01
  • 2023-03-21
  • 2012-10-21
  • 2014-02-19
  • 1970-01-01
  • 2012-05-09
  • 2017-02-15
相关资源
最近更新 更多