构建内存高效 Java 应用程序的最佳实践是什么?

What are some best practices to build memory-efficient Java applications?

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

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

在高性能设置中建模数据和对象委托时,您能否提出一些建议,以便缓解 Java 中的 "weaknesses"?

如果您有高性能约束并且需要对简单类型使用集合,您可以看看 Java.

的原始集合的一些实现。

有些是:

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

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

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

取决于应用,但一般来说

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

  • 尝试制作大 "flat" 对象,内联其他合理的子结构

  • 专门收集图元

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

  • 离开堆

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

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

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

  1. 缓存

您可以使用缓存来限制集合(缓存)可以分配的内存。通过这样做,您只需在主内存中加载最常用的条目,而无需加载整个数据集形式 disk/network/whatever。我强烈推荐 Guava Cache 因为它有很好的文档记录并且非常成熟。

  1. 持久集合

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

  1. 分析内存使用情况

最重要的是,如果您真的想知道程序内存分配的实际状态,我强烈建议您使用分析器。这样您不仅可以知道您的回收占用了多少内存,还可以了解 GC 随时间的变化情况。

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

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

  1. 内存优化数据配合网络不错

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

好吧,您可以做很多事情。

以下是一些问题和解决方案:

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

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

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

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

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;

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

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