使用ObjectInputStream#readUnshared()时发生java意外OutOfMemoryError
当我用readUnshared
从ObjectInputStream
读取大量对象时,我遇到了OOMMAT指出它的内部句柄表是罪魁祸首,OOM堆栈跟踪也是罪魁祸首(在本文末尾)。大家都说,这不应该发生。此外,OOM是否发生似乎取决于对象之前是如何写入的
根据this write-up on the topic,readUnshared
应该通过在读取期间不创建句柄表条目来解决这个问题(与readObject
相反)(这是我发现writeUnshared
和readUnshared
的方式,我以前没有注意到)
然而,根据我自己的观察,似乎readObject
和readUnshared
的行为是相同的,OOM是否发生取决于对象是否是用^{writeObject
和writeUnshared
,这并不重要,正如我之前所想的——我第一次运行测试时只是累了)。也就是说:
writeObject writeObject+reset writeUnshared writeUnshared+reset readObject OOM OK OOM OK readUnshared OOM OK OOM OK
因此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.
}
}
重新创建该程序的问题的步骤:
- 运行测试程序时
testWrite
未注释(并且testRead
未调用),堆大小设置为高,因此writeObject
不会导致OOM李> - 第二次运行测试程序时,使用默认堆大小
testRead
未注释(并且testWrite
未调用)李>
需要明确的是:我不是在同一个JVM实例中编写和阅读。我的写操作和读操作在一个单独的程序中进行。上面的测试程序乍一看可能有点误导,因为我把写测试和读测试都塞进了同一个源代码中
不幸的是,我所处的实际情况是,我有一个文件,其中包含许多用writeObject
编写的对象(没有reset
),这将需要相当长的时间来重新生成(以天为单位)(而且reset
会使输出文件变得庞大),所以如果可能的话,我希望避免这种情况。另一方面,我目前无法用readObject
读取文件,即使堆空间已达到系统上可用的最大值
值得注意的是,在我的实际情况中,我不需要对象流句柄表提供的缓存
所以我的问题是:
- 到目前为止,我的所有研究都表明
readUnshared
的行为与对象的书写方式之间没有联系。这是怎么回事 - 考虑到数据是用
writeObject
和noreset
写入的,有什么方法可以避免读取时的OOM吗
我不完全清楚为什么readUnshared
无法解决这个问题
我希望这是清楚的。我这里空着,所以可能输入了奇怪的单词
从comments开始回答以下问题:
If you're not calling
writeObject()
in the current instance of the JVM you should not be consuming memory by callingreadUnshared()
.
我所有的研究都显示了同样的结果,但令人困惑的是:
下面是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)
这是一个video of it happening(在最近的测试程序编辑之前录制的视频,视频相当于新测试程序中的
ReadMode.UNSHARED
和WriteMode.NORMAL
)下面是some test data files,其中包含30000000个对象(压缩大小是一个小小的360KB,但请注意,它会扩展到一个惊人的2.34GB)。这里有四个测试文件,每个文件都是用
writeObject
/writeUnshared
和reset
的不同组合生成的。读取行为取决于它是如何写的,独立于readObject
和readUnshared
。请注意writeObject
与writeUnshared
数据文件是逐字节相同的,我无法确定这是否令人惊讶
我一直在盯着ObjectInputStream
代码from here。我目前的嫌疑人是this line,出现在1.7和1.8中:
ObjectStreamClass desc = readClassDesc(false);
其中boolean
参数为true
表示非共享,为false
表示正常。在所有其他情况下,“unshared”标志会传播到其他调用,但在这种情况下,它被硬编码为false
,因此在读取序列化对象的类描述时,即使使用了readUnshared
,也会导致句柄添加到句柄表中。AFAICT,这是非共享标志未被传递到其他方法的唯一情况,因此我关注它
这与例如this line不同,在这里,非共享标志被传递到readClassDesc
。(如果有人想深入了解,您可以追踪从readUnshared
到这两行的调用路径。)
然而,我还没有证实这其中的任何一点是重要的,也没有解释为什么false
是硬编码的。这只是我正在研究的当前轨道,它可能会被证明毫无意义
另外,fwiw ObjectInputStream
确实有一个私有方法clear
,用于清除句柄表。我做了一个实验,每次阅读后我都称之为(通过反射),但它破坏了一切,所以这是不可能的
# 1 楼答案
writeUnShared()
仍然将null
写入其handlers
中,它将随着您写入更多对象而增长。这就是你在^{勾选:OutOfMemoryException : Memory leak in the java class ObjectOutputStream and ObjectInputStream
# 2 楼答案
没错}的句柄表的使用。如果您所在的JVM使用的是
readUnshared()
只会减少可归因于{writeObject()
,而不是writeUnshared()
,那么writeObject()
导致的句柄表使用率不会减少readUnshared()