使用 ObjectInputStream#readUnshared() 时意外的 OutOfMemoror

我从 with 读取大量对象时遇到了 OOM。MAT 将其内部句柄表指向罪魁祸首,OOM 堆栈跟踪也是如此(在本文末尾)。从各方面来看,这不应该发生。此外,OOM 是否发生似乎取决于之前对象的编写方式。ObjectInputStreamreadUnshared

根据这篇关于该主题的文章,应该通过在读取期间不创建句柄表条目来解决问题(而不是)(该写入是我发现的方式,并且,我以前没有注意到)。readUnsharedreadObjectwriteUnsharedreadUnshared

然而,从我自己的观察中可以看出,并且行为相同,OOM是否发生取决于对象是否在每次写入后都使用reset()写入(是否使用vs并不重要,正如我以前认为的那样 - 当我第一次运行测试时,我只是累了)。那是:readObjectreadUnsharedwriteObjectwriteUnshared

              writeObject   writeObject+reset   writeUnshared   writeUnshared+reset
readObject       OOM               OK               OOM                 OK
readUnshared     OOM               OK               OOM                 OK

因此,实际上是否有任何效果似乎完全取决于对象的编写方式。这对我来说是令人惊讶和意想不到的。我确实花了一些时间跟踪readUnshared代码路径,但是,考虑到它很晚并且我很累,我不清楚为什么它仍然会使用句柄空间,为什么它会取决于对象的编写方式(但是,我现在有一个初步的怀疑,尽管我还没有确认, 如下所述)。readUnshared

从我到目前为止对这个主题的所有研究来看,它似乎应该有效。writeObjectreadUnshared

以下是我一直在测试的程序:

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.
    }

}

使用该程序重新创建问题的步骤:

  1. 使用堆大小设置为较高的 s 未注释(且未调用)运行测试程序,因此不会导致 OOM。testWritetestReadwriteObject
  2. 使用默认堆大小使用未注释(且未调用)再次运行测试程序。testReadtestWrite

需要明确的是:我不是在同一个JVM实例中进行写入和读取。我的写作发生在与我的阅读分开的程序中。乍一看,上面的测试程序可能有点误导,因为我将写入和读取测试都塞进了同一个源中。

不幸的是,我所处的真实情况是,我有一个包含大量对象的文件,其中写入了(没有),这将需要相当长的时间才能重新生成(以天为单位)(并且还会使输出文件变得庞大),因此如果可能的话,我想避免这种情况。另一方面,即使堆空间已达到系统上的可用最大值,我当前也无法读取 带有 的文件。writeObjectresetresetreadObject

值得注意的是,在我的实际情况中,我不需要对象流句柄表提供的缓存。

所以我的问题是:

  1. 到目前为止,我的所有研究表明,readUnshared的行为与对象的编写方式之间没有联系。这是怎么回事?
  2. 有没有办法避免读取时的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.UNSHAREDWriteMode.NORMAL

  • 以下是一些测试数据文件,其中包含30,000,000个对象(压缩大小很小,为360 KB,但请注意,它会扩展到高达2.34 GB)。这里有四个测试文件,每个文件都使用 / 和 的各种组合生成。读取行为仅取决于它的编写方式,并且独立于 vs. 。请注意,vs数据文件是字节对字节相同的,我无法确定这是否令人惊讶。writeObjectwriteUnsharedresetreadObjectreadUnsharedwriteObjectwriteUnshared


我一直盯着这里的代码。我目前的怀疑是这条线,出现在1.7和1.8中:ObjectInputStream

ObjectStreamClass desc = readClassDesc(false);

其中,该参数用于非共享和正常。在所有其他情况下,“unshared”标志会传播到其他调用,但在这种情况下,它被硬编码为 ,从而导致在读取序列化对象的类描述时,即使使用,句柄也会被添加到句柄表中。AFAICT,这是一一次未共享标志没有传递到其他方法,因此我专注于它。booleantruefalsefalsereadUnshared

这与例如,未共享标志传递到 的行相反。(如果有人希望深入挖掘,则可以跟踪从这两条线路的调用路径。readClassDescreadUnshared

但是,我还没有确认其中任何一个都是重要的,或者为什么在那里进行了硬编码。这只是我正在研究的当前轨道,它可能被证明是没有意义的。false

此外,fwiw 确实有一个私有方法 ,用于清除句柄表。我做了一个实验,每次阅读后我都称之为(通过反射),但它只是打破了一切,所以这是不行的。ObjectInputStreamclear


答案 1

但是,如果对象是使用 而不是 编写的,则不会减少句柄表的使用率。writeObject()writeUnshared()readUnshared()

这是正确的。 仅减少可归因于 的句柄表使用率。如果您位于 正在使用的 JVM 中,而不是 ,则处理可归因于 的表使用率不会减少。readUnshared()readObject()writeObject()writeUnshared()writeObject()readUnshared()


答案 2

writeUnShared()仍然写一个到它里面,它会随着你写更多的对象而增长。这就是你得到OOM的原因。nullhandlersreadUnShared

检查这个: OutOfMemoryException : Java 类 ObjectOutputStream 和 ObjectInputStream 中的内存泄漏


推荐