从ObjectInputStream
和readUnshared
中读取大量对象时,我正在运行OOM。 MAT作为罪魁祸首指向内部处理表,OOM堆栈跟踪(在本文结尾处)也是如此。根据所有帐户,这不应该发生。此外,是否出现OOM似乎取决于以前如何写入对象。使用ObjectInputStream时发生意外的OutOfMemoryError#readUnshared()
据this write-up on the topic,readUnshared
应该解决的问题通过读取过程中没有创建句柄表项(而不是readObject
)(即写了是怎么发现的writeUnshared
和readUnshared
,这是我以前没有注意到)。
但是,从我自己的观察看来,readObject
和readUnshared
行为相同,与OOM是否发生,如果对象是书面有reset()
after each write(它不不管writeObject
VS writeUnshared
是取决于正如我以前所想的那样 - 当我第一次运行测试时,我已经很累了)。那就是:
writeObject writeObject+reset writeUnshared writeUnshared+reset readObject OOM OK OOM OK readUnshared OOM OK OOM OK
所以无论是否readUnshared
有任何影响实际上似乎是完全依赖于对象如何被写。这对我来说是令人惊讶和意外的。我确实花了一些时间来追踪readUnshared
code path,但是,并且认为它迟到了,而且我很累,它对我来说并不明显,为什么它仍然会使用句柄空间,以及为什么它要取决于对象的写法(但是,我现在有一个初步的嫌疑人,尽管我还没有确认,下面将会介绍)。
从我所有关于这个话题的研究到目前为止,看来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
的行为,以及如何写的对象之间没有任何联系。这里发生了什么? - 有没有什么办法可以避免读取OOM,因为数据是用
writeObject
和reset
写的?
我不完全确定为什么readUnshared
无法解决此问题。
我希望这很清楚。我在这里空着,所以可能会输入奇怪的单词。下面回答
从comments:
如果你不要求在JVM的当前情况下,你不应该通过调用
readUnshared()
被耗内存writeObject()
。
我所有的研究表明相同的,然而,令人混淆:
这里是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,其中包含30,000,000个对象(压缩的大小是一个小的360 KB,但被警告它扩展到高达2.34 GB)。这里有四个测试文件,每个测试文件用
writeObject
/writeUnshared
和reset
的各种组合生成。读取行为仅取决于写入方式,而与readObject
与readUnshared
无关。请注意,writeObject
与writeUnshared
数据文件是字节对字节相同的,我无法确定这是否令人惊讶。
我在ObjectInputStream
代码from here一直盯着。我现在怀疑是this line,目前在1.7和1.8:
ObjectStreamClass desc = readClassDesc(false);
如果这一boolean
参数true
非共享和false
正常。在所有其他情况下,“非共享”标志会传播到其他调用,但在这种情况下,它会被硬编码为false
,因此即使在使用readUnshared
时,也会在读取序列化对象的类描述时将句柄添加到句柄表中。 AFAICT,这是只有发生的未共享标志没有被传递给其他方法,因此我为什么关注它。
这与例如this line其中非共享标志是传递到readClassDesc
。 (如果有人想挖掘,您可以追踪从readUnshared
到这两条线路的呼叫路径。)
但是,我还没有确认这是否是重要的,或者说为什么false
是硬编码的。这只是我正在研究的当前曲目,它可能证明毫无意义。
此外,fwiw,ObjectInputStream
确实有一个私有方法,clear
,清理句柄表。我做了一个实验,我在每次阅读后都会调用它(通过反射),但它只是打破了一切,所以这是一个不行。
嗯。当你说“在同一个JVM中”你是指同一个实例吗?在我的情况下,对象是由一个单独的程序编写的,我不在同一个运行中执行'writeObject'和'readUnshared'。在测试程序中,只有'testWrite'和'testRead'中的一个应该被取消注释并在任何给定时间运行。在运行读取之前,我的序列化数据文件已经存在。 –
如果您在JVM的当前实例中未调用'writeObject()',则不应通过调用'readUnshared()'来占用内存。 – EJP
这就是我要说的!它不应该发生。 :)然而,它的确如此。我已经为指向'readUnshared'和[视频证据](https://www.youtube.com/watch?v=7-ASZJEKsYI)的问题添加了堆栈跟踪。 –