从ObjectInputStream
readUnshared
读取大量对象时,我遇到了OOM。 MAT指向其内部句柄表作为罪魁祸首,OOM堆栈跟踪(在本文末尾)也是如此。从各方面来看,这不应该发生。此外,OOM是否发生似乎取决于先前写对象的方式。
根据this write-up on the topic,readUnshared
应该通过在读取期间不创建句柄表条目来解决问题(而不是readObject
)(该记录就是我发现的{{1}我之前没有注意到的}和writeUnshared
。
然而,根据我自己的观察,readUnshared
和readObject
的行为相同,OOM是否发生取决于对象是否写与{ {3}}(如果使用readUnshared
vs writeObject
,不重要,正如我之前想的那样 - 当我第一次参加测试时,我感到很累。那就是:
writeObject writeObject+reset writeUnshared writeUnshared+reset readObject OOM OK OOM OK readUnshared OOM OK OOM OK
因此writeUnshared
是否有任何影响实际上似乎完全取决于对象是如何编写的。这对我来说是令人惊讶和意想不到的。我确实花了一些时间来追踪reset()
after each write但是,并且认为它已经很晚了我累了,我不清楚为什么它仍然会使用句柄空间以及为什么它取决于如何对象写了(但是,我现在有一个初步的怀疑,虽然我还没有确认,如下所述)。
到目前为止,我对该主题的所有研究都显示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未注释(并且testWrite
未调用)且堆大小设置为高,因此testRead
不会导致OOM。writeObject
取消注释(并且testRead
未调用),默认堆大小。要明确:我没有在同一个JVM实例中进行写入和读取。我的写作发生在与我的读取不同的程序中。由于我将写入和读取测试都塞入同一来源,因此上面的测试程序乍一看可能会有些误导。
不幸的是,我所处的真实情况是我有一个包含大量用testWrite
(没有writeObject
)编写的对象的文件,这将花费相当长的时间来重新生成(在日期的顺序)(以及reset
使输出文件变得庞大),所以我想尽可能避免这种情况。另一方面,我目前无法使用reset
读取文件,即使堆空间已达到我系统上可用的最大值。
值得注意的是,在我的实际情况中,我不需要对象流句柄表提供的缓存。
所以我的问题是:
readObject
的行为与对象的编写方式之间没有联系。这是怎么回事? readUnshared
而不是writeObject
编写的,我有什么办法可以避免读取OOM吗? 我不完全确定为什么reset
无法解决此问题。
我希望这很清楚。我在这里空着,所以可能会输入奇怪的单词。
来自readUnshared
code path的答案如下:
如果您未在JVM的当前实例中调用
readUnshared
,则不应通过调用writeObject()
来消耗内存。
我所有的研究都表现出同样的,但令人困惑的是:
这是OOM堆栈跟踪,指向readUnshared()
:
readUnshared
以下是comments(在最近的测试程序编辑之前录制的视频,视频相当于新测试程序中的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
。
以下是video of it happening,其中包含30,000,000个对象(压缩大小为360 KB,但需要警告它扩展到高达2.34 GB)。此处有四个测试文件,每个测试文件都使用WriteMode.NORMAL
/ writeObject
和writeUnshared
的各种组合生成。读取行为仅取决于它的编写方式,与reset
与readObject
无关。请注意,readUnshared
vs writeObject
数据文件是逐字节相同的,我无法确定这是否令人惊讶。
我一直盯着writeUnshared
代码some test data files。我目前的嫌疑人是from here,出现在1.7和1.8:
ObjectInputStream
其中ObjectStreamClass desc = readClassDesc(false);
参数为boolean
为非共享,true
为正常参数。在所有其他情况下,&#34; unshared&#34; flag会传播到其他调用,但在这种情况下,它会被硬编码到false
,从而导致在读取序列化对象的类描述时将句柄添加到句柄表中,即使{{1} } 用来。 AFAICT,这是非共享标志的唯一出现没有传递给其他方法,因此我专注于它。
这与例如this line,其中非共享标记 传递给false
。 (如果有人想挖掘,您可以跟踪从readUnshared
到这两行的呼叫路径。)
但是,我还没有确认任何这些是重要的,或者说明为什么readClassDesc
在那里被硬编码。这只是我目前正在研究的这条轨道,它可能被证明毫无意义。
此外,fwiw,readUnshared
确实有一个私有方法false
,它清除了句柄表。我做了一个实验,在每次阅读后我都会(通过反射)调用它,但它只是打破了一切,所以这是一个禁忌。
答案 0 :(得分:2)
但是,如果对象是使用
writeObject()
而不是writeUnshared()
编写的,那么readUnshared()
不会减少句柄表的使用。
这是正确的。 readUnshared()
仅减少归因于readObject()
的句柄表使用量。如果您使用writeObject()
而不是writeUnshared()
的JVM,则writeObject()
的句柄使用率不会减少readUnshared()
。
答案 1 :(得分:0)
writeUnShared()
仍会在null
中写一个handlers
,当你写更多对象时,readUnShared
会增长。这就是你在{{1}}上获得OOM的原因。
检查一下:OutOfMemoryException : Memory leak in the java class ObjectOutputStream and ObjectInputStream