使用 ObjectInputStream#readUnshared() 时意外的 OutOfMemoror
我从 with 读取大量对象时遇到了 OOM。MAT 将其内部句柄表指向罪魁祸首,OOM 堆栈跟踪也是如此(在本文末尾)。从各方面来看,这不应该发生。此外,OOM 是否发生似乎取决于之前对象的编写方式。ObjectInputStream
readUnshared
根据这篇关于该主题的文章,应该通过在读取期间不创建句柄表条目来解决问题(而不是)(该写入是我发现的方式,并且,我以前没有注意到)。readUnshared
readObject
writeUnshared
readUnshared
然而,从我自己的观察中可以看出,并且行为相同,OOM是否发生取决于对象是否在每次写入后都使用reset()
写入(是否使用vs并不重要,正如我以前认为的那样 - 当我第一次运行测试时,我只是累了)。那是:readObject
readUnshared
writeObject
writeUnshared
writeObject writeObject+reset writeUnshared writeUnshared+reset readObject OOM OK OOM OK readUnshared OOM OK OOM OK
因此,实际上是否有任何效果似乎完全取决于对象的编写方式。这对我来说是令人惊讶和意想不到的。我确实花了一些时间跟踪readUnshared
代码路径,但是,考虑到它很晚并且我很累,我不清楚为什么它仍然会使用句柄空间,为什么它会取决于对象的编写方式(但是,我现在有一个初步的怀疑,尽管我还没有确认, 如下所述)。readUnshared
从我到目前为止对这个主题的所有研究来看,它似乎应该有效。writeObject
readUnshared
以下是我一直在测试的程序:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class OOMTest {
// This is the object we'll be reading and writing.
static class TestObject implements Serializable {
private static final long serialVersionUID = 1L;
}
static enum WriteMode {
NORMAL, // writeObject
RESET, // writeObject + reset each time
UNSHARED, // writeUnshared
UNSHARED_RESET // writeUnshared + reset each time
}
// Write a bunch of objects.
static void testWrite (WriteMode mode, String filename, int count) throws IOException {
ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(filename)));
out.reset();
for (int n = 0; n < count; ++ n) {
if (mode == WriteMode.NORMAL || mode == WriteMode.RESET)
out.writeObject(new TestObject());
if (mode == WriteMode.UNSHARED || mode == WriteMode.UNSHARED_RESET)
out.writeUnshared(new TestObject());
if (mode == WriteMode.RESET || mode == WriteMode.UNSHARED_RESET)
out.reset();
if (n % 1000 == 0)
System.out.println(mode.toString() + ": " + n + " of " + count);
}
out.close();
}
static enum ReadMode {
NORMAL, // readObject
UNSHARED // readUnshared
}
// Read all the objects.
@SuppressWarnings("unused")
static void testRead (ReadMode mode, String filename) throws Exception {
ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(filename)));
int count = 0;
while (true) {
try {
TestObject o;
if (mode == ReadMode.NORMAL)
o = (TestObject)in.readObject();
if (mode == ReadMode.UNSHARED)
o = (TestObject)in.readUnshared();
//
if ((++ count) % 1000 == 0)
System.out.println(mode + " (read): " + count);
} catch (EOFException eof) {
break;
}
}
in.close();
}
// Do the test. Comment/uncomment as appropriate.
public static void main (String[] args) throws Exception {
/* Note: For writes to succeed, VM heap size must be increased.
testWrite(WriteMode.NORMAL, "test-writeObject.dat", 30_000_000);
testWrite(WriteMode.RESET, "test-writeObject-with-reset.dat", 30_000_000);
testWrite(WriteMode.UNSHARED, "test-writeUnshared.dat", 30_000_000);
testWrite(WriteMode.UNSHARED_RESET, "test-writeUnshared-with-reset.dat", 30_000_000);
*/
/* Note: For read demonstration of OOM, use default heap size. */
testRead(ReadMode.UNSHARED, "test-writeObject.dat"); // Edit this line for different tests.
}
}
使用该程序重新创建问题的步骤:
- 使用堆大小设置为较高的 s 未注释(且未调用)运行测试程序,因此不会导致 OOM。
testWrite
testRead
writeObject
- 使用默认堆大小使用未注释(且未调用)再次运行测试程序。
testRead
testWrite
需要明确的是:我不是在同一个JVM实例中进行写入和读取。我的写作发生在与我的阅读分开的程序中。乍一看,上面的测试程序可能有点误导,因为我将写入和读取测试都塞进了同一个源中。
不幸的是,我所处的真实情况是,我有一个包含大量对象的文件,其中写入了(没有),这将需要相当长的时间才能重新生成(以天为单位)(并且还会使输出文件变得庞大),因此如果可能的话,我想避免这种情况。另一方面,即使堆空间已达到系统上的可用最大值,我当前也无法读取 带有 的文件。writeObject
reset
reset
readObject
值得注意的是,在我的实际情况中,我不需要对象流句柄表提供的缓存。
所以我的问题是:
- 到目前为止,我的所有研究表明,
readUnshared
的行为与对象的编写方式之间没有联系。这是怎么回事? - 有没有办法避免读取时的OOM,因为数据是用
写对象
写入的,并且没有重置
?
我不完全确定为什么不能在这里解决问题。readUnshared
我希望这很清楚。我在这里空着跑,所以可能输入了奇怪的单词。
从以下答案的评论中:
如果不在 JVM 的当前实例中调用,则不应通过调用 来消耗内存。
writeObject()
readUnshared()
我所有的研究表明,这是一样的,但令人困惑的是:
-
下面是 OOM 堆栈跟踪,指向:
readUnshared
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.io.ObjectInputStream$HandleTable.grow(ObjectInputStream.java:3464) at java.io.ObjectInputStream$HandleTable.assign(ObjectInputStream.java:3271) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1789) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350) at java.io.ObjectInputStream.readUnshared(ObjectInputStream.java:460) at OOMTest.testRead(OOMTest.java:40) at OOMTest.main(OOMTest.java:54)
这是它发生的视频(在最近的测试程序编辑之前录制的视频,视频相当于和在新的测试程序中)。
ReadMode.UNSHARED
WriteMode.NORMAL
以下是一些测试数据文件,其中包含30,000,000个对象(压缩大小很小,为360 KB,但请注意,它会扩展到高达2.34 GB)。这里有四个测试文件,每个文件都使用 / 和 的各种组合生成。读取行为仅取决于它的编写方式,并且独立于 vs. 。请注意,vs数据文件是字节对字节相同的,我无法确定这是否令人惊讶。
writeObject
writeUnshared
reset
readObject
readUnshared
writeObject
writeUnshared
我一直盯着这里的代码。我目前的怀疑是这条线,出现在1.7和1.8中:ObjectInputStream
ObjectStreamClass desc = readClassDesc(false);
其中,该参数用于非共享和正常。在所有其他情况下,“unshared”标志会传播到其他调用,但在这种情况下,它被硬编码为 ,从而导致在读取序列化对象的类描述时,即使使用,句柄也会被添加到句柄表中。AFAICT,这是唯一一次未共享标志没有传递到其他方法,因此我专注于它。boolean
true
false
false
readUnshared
这与例如,未共享标志传递到 的行相反。(如果有人希望深入挖掘,则可以跟踪从这两条线路的调用路径。readClassDesc
readUnshared
但是,我还没有确认其中任何一个都是重要的,或者为什么在那里进行了硬编码。这只是我正在研究的当前轨道,它可能被证明是没有意义的。false
此外,fwiw 确实有一个私有方法 ,用于清除句柄表。我做了一个实验,每次阅读后我都称之为(通过反射),但它只是打破了一切,所以这是不行的。ObjectInputStream
clear