【问题标题】:Optimizing java heap usage by String using StringBuffer , StringBuilder , String.intern()使用 StringBuffer 、 StringBuilder 、 String.intern() 通过 String 优化 Java 堆使用
【发布时间】:2014-12-05 01:40:27
【问题描述】:

我正在使用 VisualVM 监控大型 java 应用程序的性能和 CPU。 当我查看它的内存配置文件时,我发现最大堆(大约 50%)正在被 char 数组用完。

以下是内存配置文件的截图:

在任何给定时间的内存配置文件中,我看到大约 9000 个 char[] 对象。

应用程序接受一个大文件作为输入。该文件大约有 80 行,每行包含 15-20 个分隔的配置选项。应用程序解析文件并将这些行存储在字符串的 ArrayList 中。然后它解析这些字符串以获取每个服务器的单独配置选项。

应用程序还经常将每个事件记录到控制台。

字符串的 Java 实现在内部使用 char[] 以及对数组和 3 个整数的引用

从互联网上的不同帖子看来,StringBuffer、StringBuilder、String.intern() 是更节省内存的数据类型。

它们与 java.lang.String 相比如何?有人对它们进行基准测试吗?如果应用程序使用多线程(它确实如此)它们是安全的选择吗?

【问题讨论】:

  • 你实际使用字符串是为了什么?这些都不会“自动”更有效。我们需要更多细节。
  • @JarrodRoberson 你读过这个问题吗?它的标题是优化堆使用。链接问题中没有堆,当谈到内存时,它们意味着 GC 开销。 这个问题可能与其他问题重复,肯定是一个混淆问题,但与链接问题大多无关。您以粗体引用的部分只是问题的一小部分。
  • StringsStrings 在 Java 中是 Strings。它们如何成为字符串的实例相对不重要,因为当您释放 StringBuffer/StringBuilder 时,它就有资格被收集。垃圾收集取决于两件事,它们是否可访问以及内存压力是多少? The rest is pretty much completely out of your control.
  • 如果你想优化堆的使用,确保你的List 中的StringsList 中删除,并且所有的引用在你完成它们后立即被释放。对此没有深入的分析,即 Java 101。除非你得到OutOfMemoryExceptions,否则这里没有问题。看看Related 侧边栏,Stackoverflow 上已经讨论过这个问题。
  • 你为什么要将文件读入 ArrayList 呢?不能逐行处理吗?

标签: java string optimization heap-memory visualvm


【解决方案1】:

我所做的是拥有一个或多个字符串池。我这样做是为了 a) 如果池中有一个字符串,则不要创建新字符串;b) 减少保留的内存大小,有时减少 3-5 倍。您可以自己编写一个简单的字符串内部,但我建议您首先考虑如何读取数据以确定最佳解决方案。这很重要,因为如果您没有有效的解决方案,您很容易让事情变得更糟。

正如 EJP 指出的那样,一次处理一行更有效,就像在阅读时解析每一行一样。即intdouble 占用的空间比相同的字符串要少得多(除非您的重复率非常高)


这是一个 StringInterner 的示例,它采用 StringBuilder 来避免不必要地创建对象。您首先使用文本填充回收的 StringBuilder,如果与该文本匹配的字符串在内部,则返回该字符串(或 StringBuilder 的 toString()。)好处是您只创建对象(并且不超过需要)当您看到一个新字符串(或至少一个不在数组中)时,这可以获得 80% 到 99% 的命中率,并在加载许多数据字符串时显着减少内存消耗(和垃圾)。

public class StringInterner {
    @NotNull
    private final String[] interner;
    private final int mask;

    public StringInterner(int capacity) {
        int n = nextPower2(capacity, 128);
        interner = new String[n];
        mask = n - 1;
    }

    @Override
    @NotNull
    public String intern(@NotNull CharSequence cs) {
        long hash = 0;
        for (int i = 0; i < cs.length(); i++)
            hash = 57 * hash + cs.charAt(i);
        int h = hash(hash) & mask;
        String s = interner[h];
        if (isEqual(s, cs))
            return s;
        String s2 = cs.toString();
        return interner[h] = s2;
    }

    static boolean isEqual(@Nullable CharSequence s, @NotNull CharSequence cs) {
        if (s == null) return false;
        if (s.length() != cs.length()) return false;
        for (int i = 0; i < cs.length(); i++)
            if (s.charAt(i) != cs.charAt(i))
                return false;
        return true;
    }

    static int nextPower2(int n, int min) {
        if (n < min) return min;
        if ((n & (n - 1)) == 0) return n;
        int i = min;
        while (i < n) {
            i *= 2;
            if (i <= 0) return 1 << 30;
        }
        return i;
    }

    static int hash(long n) {
        n ^= (n >> 43) ^ (n >> 21);
        n ^= (n >> 15) ^ (n >> 7);
        return (int) n;
    }
}

这个类的有趣之处在于它在传统意义上不是线程安全的,但在并发使用时会正常工作,实际上当多个线程对数组内容具有不同视图时可能会更有效。

【讨论】:

  • 在这些情况下,Java 中的天真的对象池比任何东西都更有害,如果有人问这样一个基本问题,他们肯定不会得到一个自我实现的 String 池正确。对于提出这样一个问题的人来说,这是一个糟糕的建议,这表明对内存在不同 JVM 实现中的工作方式缺乏深入的了解。池是一种缓存形式,缓存和并发性很难正确,即使是“高级”Java 人员也是如此。第二段是有效的,但正如你所指出的,它只是在肯定别人已经说过的话。
  • @JarrodRoberson 我同意,您需要有这样做的经验,但如果您从不尝试,您将无法获得这种经验。顺便说一句,不可变对象池非常容易同时实现。池化可变对象,这很难做到正确和高效。
  • @JarrodRoberson +1。 OP应该首先明确他们需要什么,至少对他们自己来说。但是没有必要再次实现interners
  • @PeterLawrey 我正要写这样一个池是多么万无一失,但注意到它很容易出错......只需使用不同步的LinkedHashMap 并在@987654327 中获得无限循环@.
  • 你让我明白了,你的问题域与 OP 问题域相差几个数量级
猜你喜欢
  • 2011-02-27
  • 2012-07-01
  • 1970-01-01
  • 2022-06-10
  • 2014-08-18
  • 2017-07-18
  • 1970-01-01
  • 2017-05-04
  • 2017-05-27
相关资源
最近更新 更多