欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

由 ObjectInputStream 所引起的 Java 内存泄漏问题

程序员文章站 2022-07-05 19:57:02
Java 的 ObjectInputStream 和 ObjectInputStream 各自保留一个对已发送/已接收对象的引用的列表。就是这些引用,会阻止垃圾收集器对这些对象内存的释放。当新对象的数量不断增长时(比方说在服务器中),最终将抛出"Java.lang.OutOfMemoryError"。解决办法就是使用 writeUnshared() 和 readUnshared() 方法来取代 writeObject() 和 readObject() 方法。介绍怎样在 Java 中创建一个内存泄漏?这...

Java 的 ObjectOutputStream 和 ObjectInputStream 各自保留一个对已发送/已接收对象的引用的列表。就是这些引用,会阻止垃圾收集器对这些对象内存的释放。
当新对象的数量不断增长时(比方说在服务器中),最终将抛出"Java.lang.OutOfMemoryError"。解决办法就是使用 writeUnshared() 和 readUnshared() 方法来取代 writeObject() 和 readObject() 方法。
介绍
怎样在 Java 中创建一个内存泄漏?这是一个很好地面试题,因为 Java 编程中由于底层的自动垃圾收集功能而无需担心内存释放问题。但在复杂的情况下你仍然会遭遇 Java.lang.OutOfMemoryError 问题。这里就描述了一种这样的情况:由 java.io.ObjectInputStream 和 java.io.ObjectOutputStream 所实现的 Java 序列化机制所引起的内存泄漏。
原因
ObjectOutputStream 和 ObjectInputStream 都各自维护了一个对其已发送/已接收对象的引用表。所以当 ObjectOutputStream 重新发送某对象时,可以仍发送该对象的句柄,相应的 ObjectInputStream 则将接收到的句柄转换为先前接收到的对象的引用。Java 文档中这样描述:

使用共享机制 (…) 对单个对象的多个引用进行编码。

可以说,Java 的这一 feature 减少了带宽和内存的使用量,但也仅适用于通过链接定期重新发送对象的程序中。但在通过链接发送新对象的情况下,此功能除了不必要的(不断增长的)引用表以外没有任何作用。
垃圾收集器的工作原理就是清理不再被引用的对象。但由于 ObjectInputStream 始终保持了对其所接收的每个对象的引用,它阻止了垃圾收集器对通过流所接收的任何对象的清理。就在诸如服务器之类的程序中,它们接收到越来越多的对象,最终内存将被耗尽,引发 OutOfMemoryError。
情况识别和分析
OOM 可能有很多种不同的原因所造成。为了能够确定 OOM 是上文所讨论的 ObjectInputStream 相关 feature 所造成的,我们首先得来分析该进程的 heap dump。有很多种办法可以针对一个 Java 进程生成一个 heap dump,也有很多种办法来分析这些 dump 文件。我们可以通过将 JVM 命令行选项添加到进程执行命令行来生成 dump。还可以针对一个已经在运行中的程序来生成一个 dump。其中,通过添加命令行选项的方式可以在有 OOM 错误时生成 dump 文件(当然,还有一些其他命令行选项通过信号控制方式来在任意时间生成 dump,不管程序状态如何)。命令行选项如下:

-XX:+HeapDumpOnOutOfMemoryError

在 OOM 抛出时,一个名为 java_pid.hprof 的文件将会在该进程的执行目录所生成。有 n 多 dump 分析程序可以对 dump 文件进行分析,本文只对 jvm 原生自带的 VisualVM 进行介绍。
在 VisualVM 中,打开你收集到的 hprof 文件。在 classes 选项卡中,可以找到所有的类,这些类默认是按照 dump 中所存在的它们所拥有的实例数进行排序。当然,顶部的类极可能就是实例数暴多且导致内存错误的那个类。按字母顺序对类列表进行排序也是很有帮助的,这样可以将同一包的类分组在一起,进而可以对它们进行比较。
双击该列表中的一个类,视图切换至 instances 选项卡。对于每个实例,都有两个窗口。下边的那个保存指向该实例的参考路径。每个路径的边缘是一个垃圾收集 root(由三角形标记)。这样每个边缘点都是一个对象,该对象持有一个引用,防止 GC 对该对象的清理。如果很多这种爆炸的类实例都指向了一个 ObjectInputStream 对象,那么导致 OOM 的罪魁祸首可能就是本文所讨论的主题。
解决方案
首先,不要做的事情:尝试让 GC 更好的工作是没有好处的,比如调用

System.gc()

命令。OOM 事件的先决条件就是 GC 已经尽其最大努力分配了所需要的内存,但并未成功分配。
为了使对象流不再使用上文讨论的引用机制,I/O 方法:

writeObject()
readObject()

需要替换为以下方法:

writeUnshared()
readUnshared()

不同于 readObject() 方法的是,readUnshared() 方法不会在 ObjectInputStream 的引用列表中保存引用,因此能够防止内存泄漏。需要注意的是,如果传输对象已经实现了来自 Serializable 接口的 readResolve() 方法,则该对象的引用可能还是会被传递并保留在引用列表中。可以在 VisualVM 中对该引用的持有者进行进一步分析。
还有一种解决方案是在每次写入后调用 ObjectOutputStream reset() 方法。这将具有相同的效果。
关于使用 C vs. Java 所开发服务器的讨论
本文所讨论的内存泄漏很难被预料到。具有自动垃圾收集功能的语言进行开发的本质是,开发人员无需投入精力来了解处于不同状态的程序的内存状态。当然,这也是自动垃圾收集的优势之一。使用自动决策算法的陷阱是其决策必须保守。当关注 GC 时,这意味着在不确定是否需要清除内存时,它将始终选择不清除可能不需要的内存,而不会冒清除所需内存的风险。这就是为什么每个引用对象都不会被清除的原因,尽管程序不会使用所有引用对象。在由 ObjectInputStream 引起泄漏的情况下,对于开发人员来说,引用是完全隐藏和意外的。导致内存泄漏的其他情况可能是,例如,当保存对静态对象的引用数量不断增长时(永远不会清除自身),或者在传递内部类对象时(保留对外部类的隐式引用)。这些示例对开发人员来说更为明确,但泄漏被提前发现的机会仍然很小。
另一种选择是不使用自动垃圾收集,而是让开发人员自己处理所有内存,就像在 C 和 C++ 中进行开发时所做的那样。这种开发需要开发人员做更多的工作,但是对程序的内存状态的理解水平要高得多。它虽然也不能防止内存错误的发生,但是可以使开发人员在分析它们时更了解发生了什么。
C 语言和 Java 语言对开发人员对内存责任的不同态度的另一个例子是,在 C 网络程序中,总是很清楚通过链接发送的每个数据段的大小,而不是 Java 的 Object 流那样对其中完全不清楚。在 C 语言中,开发人员甚至还必须考虑操作系统对数据结构进行的地址对齐(使用 attribute((packed)))。
大多数服务器程序都是用 Java 或 Python 等带有自动垃圾收集器的语言所编写的。对于大部分人来讲,节省开发人员的时间比这里提到的陷阱更有价值。
原文链接:Java memory leak caused by ObjectInputStream

本文地址:https://blog.csdn.net/defonds/article/details/110670970