【问题标题】:What are some best practices to build memory-efficient Java applications?构建内存高效的 Java 应用程序有哪些最佳实践?
【发布时间】:2015-04-21 08:41:37
【问题描述】:

Java 程序可能非常消耗内存。例如,Double 对象有 24 个字节:8 个字节的数据和 16 个字节的 JVM 强加开销。通常,表示原始类型的对象非常昂贵。

Java 标准库中的任何集合都会发生同样的情况。甚至还有一些违反直觉的事实,例如 HashSet 比 HashMap 更需要内存,因为 HashSet 内部包含 HashMap (http://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html)。

在对高性能设置中的数据建模和对象委托时,您能否提出一些建议,以减轻 Java 的这些“弱点”?

【问题讨论】:

标签: java performance memory-leaks


【解决方案1】:

我用来减少内存的一些技巧:

  • 制作您自己的防止装箱的 IntArrayList (etc) 类
  • 制作您自己的 IntHashMap (etc) 类,其中键是基元
  • 使用 nio 的 ByteBuffer 有效地存储大型数据数组(在本机内存中,在堆外)。它就像一个字节数组,但包含以任意偏移量从缓冲区存储/检索所有原始类型的方法(以内存换取速度)
  • 不要使用池化,因为池会显式地保持未使用的实例处于活动状态。
  • 很少使用线程,它们非常消耗内存(在本机内存中,在堆外)
  • 在制作大字符串的子字符串并丢弃原始字符串时,子字符串仍然引用原始字符串。所以使用new String 来处理旧的大字符串。
  • 线性数组小于多维数组,如果除最后一维之外的所有维度都是 2 的幂,则计算索引的速度最快:array[x|y<<4] 用于 16xN 数组。
  • 初始化集合和StringBuilder 并选择初始容量,以防止在典型情况下进行内部重新分配。
    • 使用StringBuilder 代替字符串连接,因为编译的类文件使用new StringBuilder() 没有初始容量来连接字符串。

【讨论】:

    【解决方案2】:

    取决于应用程序,但一般来说

    • 在(并行)基元数组中布局数据结构

    • 尝试制作大的“扁平”对象,内联其他合理的子结构

    • 特化基元集合

    • 重用对象,使用对象池,ThreadLocals

    • 堆外

    我不能说这些做法是“最好的”,因为不幸的是,它们会让您受苦,失去使用 Java 的意义,降低代码库的灵活性、可支持性、可靠性、可测试性和其他“良好”属性。

    但是,它们确实可以降低内存占用和 GC 压力。

    【讨论】:

    • 好建议。堆外是什么意思?
    • @JadieldeArmas ByteBuffer.allocateDirect(); FileChannel.map();或更高级的东西,例如 OpenHFT 的东西(见Chronicle Map,Chronicle Queue)
    【解决方案3】:

    Java 中容易被忽视的内存问题之一是内存泄漏。 Nicholas Greene 已经向您指出了内存分析。

    许多人认为 Java 的垃圾收集可以防止内存泄漏,但事实并非如此 - 只需要一个被遗忘的引用某处就可以永久保留对象。矛盾的是,尝试优化您的程序可能会带来更多的内存泄漏机会,因为您最终会得到更复杂的数据结构。

    如果你正在实现一个内存泄漏的例子,例如,一个堆栈:

    Integer stack[];
    stack = new Integer[10];
    int stackPtr = 0;
    
    // a few push operation on our stack.
    stack[stackPtr++] = new Integer(5);
    stack[stackPtr++] = new Integer(3);
    
    // and pop from the stack again
    --stackPtr;
    --stackPtr;
    
    // at this point, the stack is logically empty, but
    // the Integer objects are still referenced by the array,
    // and are basically leaked.
    

    正确的解决方案是:

    stack[--stackPtr] = null;
    

    【讨论】:

      【解决方案4】:

      如果您有较高的性能限制并且需要将集合用于简单类型,您可以查看 Java 的 Primitive Collections 的一些实现。

      有些是:

      另外,作为参考,看看这个问题:Why can Java Collections not directly store Primitives types?

      【讨论】:

      • 建议已过时/具有误导性。 Gs 集合/fastutil/koloboke/hppc-rt.
      【解决方案5】:

      Luís Bianchin 已经为您提供了一些在 Java 中实现最佳集合的库。 不过,您似乎特别关心 Java 集合的内存分配。在这种情况下,有一些非常简单的替代方案。

      1. 缓存

      您可以使用缓存来限制集合(缓存)可以分配的内存。通过这样做,您只需将最常用的条目加载到主内存中,而无需从磁盘/网络/其他任何地方加载整个数据集。我强烈推荐 Guava Cache,因为它有很好的文档记录并且非常成熟。

      1. 永久收藏

      有时缓存不能解决您的问题。例如,在 ETL 解决方案中,您可能知道每个条目只会加载一次。对于这种情况,我建议使用持久性集合。这些是磁盘存储的集合,比传统数据库快得多,但有很好的 Java API。 MapDB 和 PCollections 对我来说是最好的库。

      1. 配置文件内存使用情况

      除此之外,如果您真的想知道程序内存分配的实际状态,我强烈建议您使用分析器。这样,您不仅会知道集合占用了多少内存,而且还会知道 GC 随着时间的推移如何表现。

      事实上,如果确实存在内存问题,您应该只尝试 Java 集合和数据结构的替代方案,而这是分析器可以告诉您的。

      JDK 有一个称为 VisualVM 的分析器,它做得很好。不过,如果您负担得起,我建议您使用商业分析器。与 VisualVM 相比,商业分析器通常对应用程序性能的影响较小。

      1. 内存优化数据非常适合网络。

      最后,它与您的问题并不严格相关,但密切相关。如果您想将 Java 对象序列化为最佳二进制表示,我建议您 Java 中的 Google Protocol Buffers。协议缓冲区非常适合使用尽可能少的带宽并具有非常快速的编码/解码的网络传输数据结构。

      【讨论】:

        【解决方案6】:

        你可以做很多事情。

        这里有几个问题和解决方案:

        1. 当你在java中改变一个字符串的值时,这个字符串实际上并没有被覆盖。相反,会创建一个新字符串来替换旧字符串。但是,旧字符串仍然存在。当有效地使用 RAM 是一个问题时,这可能是一个问题。以下是解决此问题的一些方法:

          • 当使用字符串来指定对象的“状态”或其他任何只能具有特定可能值集的内容时,请勿使用字符串。而是使用枚举。如果您还不知道枚举是什么或如何使用枚举,here's a link to a tutorial on what enums are and how to use them!
          • 如果您使用字符串作为变量,该变量的值会在程序中的某个时间点发生变化,请不要像通常那样定义字符串。相反,请使用 java.lang 包中的 StringBuilder 类。 StringBuilder 是一个用于创建字符串并更改其值的类。此类处理字符串的方式与平常不同。当它用于更改字符串的值时,StringBuilder 不会创建具有不同值的重复字符串来替换旧字符串,它实际上是更改了原始字符串的值。因此,由于您没有创建重复的字符串,因此可以节省 RAM。 Here is a link to to the StringBuilder class in the java api.
        2. 写入器和读取器对象(例如 fileWriters 和 fileReaders)也会占用 RAM。如果你有很多,这也可能导致问题。以下是一些解决方案:

          • 所有读取器和写入器对象都有一个名为 close() 的方法。正如你可能猜到的,它关闭了 writer 或 reader 对象。它所做的只是摆脱 reader 或 writer 对象。每当您有一个读取器或写入器对象并且当您知道您将永远不会再使用读取器或写入器对象时到达代码中的点时,请使用此方法。它将摆脱读取器或写入器对象并释放一些 RAM。
        3. java 中的每个对象都占用内存。当你有一个不再使用的对象时,把它放在身边不是很方便。

          • Object 类有一个名为 finalize() 的方法。此方法与 reader 和 writer 对象中的 close() 方法具有相同的效果。当您不再使用某个对象时,请使用 finalize() 方法摆脱它并释放一些 RAM。

        【讨论】:

        • 在第 3 点上,finalize 实际上更类似于 C++ 中的析构函数。你不应该调用它;垃圾收集器将在收集对象之前不久调用它。但是,它不是很可靠。基本上没有很好的方法来预测何时或如何调用此方法(甚至根本没有调用它)。
        【解决方案7】:

        提防早期优化。 见When is optimisation premature?

        虽然不知道您的应用程序或运行时环境的确切要求,但根据我的经验,java 能够处理我扔给它的任何东西。如果性能或垃圾收集(您标记了内存泄漏)是一个问题,那么对您的演示/概念验证应用程序进行一些分析可能是值得的。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-07-09
          • 2011-02-18
          • 2020-11-14
          • 2015-10-02
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多