【问题标题】:Java 8, memory wasted by duplicate stringsJava 8,重复字符串浪费了内存
【发布时间】:2021-02-07 07:03:52
【问题描述】:

我正在调查在 Java 8 JVM 上运行的 Grails 3.3.10 服务器中的内存泄漏。我从内存不足的生产服务器中提取了一个堆转储,并使用JXRay 对其进行了分析。 html 报告说,一些内存浪费在重复的字符串上,开销为 19.6%。其中大部分浪费在空字符串“”的重复上,并且主要来自数据库读取。我对此有两个问题。

  1. 我应该开始实习字符串还是操作成本太高不值得?

  2. 我的很多代码都处理来自 elasticsearch 的深度嵌套的 JSON 结构,我不喜欢代码的脆弱性,所以我创建了一个小的帮助程序类以避免在访问来自 json 的数据时出现拼写错误。

public static final class S {
    public static final String author      = "author";
    public static final String aspectRatio = "aspectRatio";
    public static final String userId      = "userId";
    ... etc etc

这有助于我避免这样的错别字:

    Integer userId = json.get("userid"); // Notice the lower case i. This returns null and fails silently
    Integer userId = json.get(S.userId); // If I make a typo here the compiler will tell me.

我对此感到相当高兴,但现在我在猜测自己。出于某种原因,这是一个坏主意吗?我还没有看到其他人这样做。这不应该导致创建任何重复的字符串,因为它们被创建一次,然后在我的解析代码中被引用,对吧?

【问题讨论】:

  • 如果你的目标是减少内存消耗,那么是的,也许你应该检查你的字符串,比如if (str.length == 0) str = ""; //this will use the intern version

标签: java string memory-leaks


【解决方案1】:

String 持有类的问题是您使用的语言违背了它的语言设计。

类应该引入类型。没有提供实用程序的类型,因为它是“可以用字符串表示的所有内容”类型很少有用。虽然在许多程序中都会出现这种情况,但通常它们会引入比“所有东西都在这里”更多的行为。例如,语言环境数据库提供不同语言的替换字符串。

我会从制定合理的枚举开始。错误消息可能很容易转换为枚举,它具有简单的自动转换字符串表示。这样您就可以获得“错字检测”和内置分类。

 DiskErrors.DISK_NOT_FOUND
 Prompts.ASK_USER_NAME
 Prompts.ASK_USER_PASSWORD

此类更改的副作用可能会达到您想要的目标;但请注意,此类更改通常表明可读性下降。

可读性不是你认为容易阅读的东西,而是从未使用过代码的人认为容易阅读的东西。

如果我看到“未找到您选择的硬盘驱动器”的问题,那么我会查看代码库中的字符串“未找到您选择的硬盘驱动器”。这可能会让我在两个地方:

  1. 在代码块中出现了错误消息。
  2. 在将该字符串映射到名称的表中。
  3. 在许多代码块中都会引发相同的错误消息。

通过表映射,我可以进行第二次搜索,搜索名称的使用位置。这可以让我遇到一些情况:

  1. 在一处使用。
  2. 在很多地方都有使用。

有了一个地方,就会出现一种代码维护问题。您现在拥有一个未被代码的任何其他部分使用的常量,该常量被维护在一个不靠近使用它的地方。这意味着要进行任何需要完全了解影响的更改,必须牢记远程常量的值,以了解逻辑更改是否应与更新的错误消息相结合。导致额外出错机会的不是错误消息的更新,而是它已从正在处理的代码中删除。

对于多个位置,我必须循环遍历所有匹配项,这与第一步中的多个字符串匹配基本相同。因此,该表并不能帮助我找到错误的根源,它只是添加了与解决问题无关的额外步骤。

现在,该表在一种情况下确实具有明显的好处:当针对特定类型问题的所有消息应同时更新时。问题是,这种情况很少见,而且不太可能发生。更有可能发生的是错误消息对于特定场景不够具体;但是,经过另一次“扫描所有使用它的地方”对于其他场景是正确的。因此错误消息被拆分,而不是就地更新,因为查找表强制执行的耦合意味着如果不创建新的错误消息,就无法修改某些错误消息。

此类问题来自开发人员在吸引开发人员的功能方面的失误。 在您的情况下,您正在构建一个反错字系统。让我提供一个更好的解决方案;因为错别字是真实存在的,也是一个真正的问题。

编写一个单元测试来捕获预期的输出。您很少会以完全相同的方式写两次相同的错字。是的,这是可能的,但协调的拼写错误会对两个系统产生相同的影响。如果您在查找表中引入拼写错误,并在使用中引入它,好处将是一个工作程序,但很难称其为高质量的解决方案(因为拼写错误没有受到保护并且存在于重复)。

在将代码提交到构建系统之前对其进行审核。评论可能会失控,尤其是对于不灵活的评论者,但好的评论应该评论“你拼错了”。如果可能的话,作为一个团队来审查代码,这样你就可以在他们制作他们的 cmets 时指出你的想法。如果你很难与人合作(或者他们很难与人合作),你会发现同行评审很困难。如果发生这种情况,我很抱歉,但如果您能获得良好的同行评审,这是针对这些问题的第二“最佳”防御措施。

抱歉,回复的篇幅过长,但我希望这能让您有机会记住从解决方案“退后一步”,看看它如何影响您对代码的未来操作。

至于"" 字符串,关注为什么设置它可能比用实习修补问题更有效地构建更好的产品(但我无法访问您的代码库,所以我可能是错的!)

祝你好运

【讨论】:

    【解决方案2】:

    Q1:我应该开始实习字符串还是操作成本太高不值得?

    如果没有关于字符串是如何创建的以及它们的典型生命周期的更多信息,很难说,但一般答案是否定的。通常不值得。

    (实习并不能解决你的内存泄漏问题。)

    以下是一些原因(恐怕有点手无寸铁):

    • 实习字符串不会阻止创建您实习的字符串。您的代码仍然需要创建它,GC 仍然需要收集它。

    • 有一个隐藏的数据结构来组织内部字符串。那使用内存。检查一个字符串是否在内部数据结构中并在需要时添加它也会消耗 CPU。

    • GC 需要对内部数据结构做一些特殊的(弱引用之类的)事情,以防止它泄漏。这是开销。

    • 实习字符串的寿命往往比非实习字符串长。它更有可能被“旧”堆使用,这会导致其生命周期更长……因为“旧”堆的 GC 频率较低。

    如果您使用 G1 收集器并且重复字符串通常存在很长时间,您可能需要尝试启用 G1GC 字符串重复数据删除(请参阅here)。否则,您最好让 GC 处理字符串。 Java GC 的设计是为了有效地处理大量对象(例如字符串)被创建并很快被丢弃。

    如果创建 Java 字符串的是您的代码,那么可能值得对其进行调整以避免创建新的零长度字符串。根据@ControlAltDel 的评论手动实习零长度字符串可能不值得。

    最后,如果您要尝试以某种方式减少重复,我强烈建议您进行设置,以便衡量优化的效果:

    • 你真的节省内存吗?
    • 这会影响 GC 运行的速率吗?
    • 这会影响 GC 暂停吗?
    • 它会影响请求时间/吞吐量吗?

    如果测量结果表明优化没有帮助,您需要退出。


    Q2:出于某种原因,这是一个坏主意吗?这不应该导致创建任何重复的字符串,因为它们被创建一次,然后在我的解析代码中被引用,对吧?

    我想不出任何不这样做的理由。它当然不会直接导致重复字符串的创建。

    另一方面,您不会仅仅通过这样做来减少字符串重复。表示字面量的字符串会自动被实习。

    【讨论】:

      猜你喜欢
      • 2011-02-13
      • 1970-01-01
      • 2016-03-03
      • 2016-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多