有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

使用ObjectInputStream#readUnshared()时发生java意外OutOfMemoryError

当我用readUnsharedObjectInputStream读取大量对象时,我遇到了OOMMAT指出它的内部句柄表是罪魁祸首,OOM堆栈跟踪也是罪魁祸首(在本文末尾)。大家都说,这不应该发生。此外,OOM是否发生似乎取决于对象之前是如何写入的

根据this write-up on the topicreadUnshared应该通过在读取期间不创建句柄表条目来解决这个问题(与readObject相反)(这是我发现writeUnsharedreadUnshared的方式,我以前没有注意到)

然而,根据我自己的观察,似乎readObjectreadUnshared的行为是相同的,OOM是否发生取决于对象是否是用^{} after each write写的(如果使用writeObjectwriteUnshared,这并不重要,正如我之前所想的——我第一次运行测试时只是累了)。也就是说:

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

因此readUnshared是否有任何影响实际上似乎完全取决于对象是如何编写的。这让我感到惊讶和意外。我确实花了一些时间在^{} code path中追踪,但是,尽管时间很晚,我也很累,但我不清楚为什么它仍然会使用句柄空间,为什么它会取决于对象的编写方式(不过,我现在有一个初步的怀疑,尽管我还没有确认,如下所述)

从我迄今为止对这个话题的所有研究来看,似乎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. 运行测试程序时testWrite未注释(并且testRead未调用),堆大小设置为高,因此writeObject不会导致OOM
  2. 第二次运行测试程序时,使用默认堆大小testRead未注释(并且testWrite未调用)

需要明确的是:我不是在同一个JVM实例中编写和阅读。我的写操作和读操作在一个单独的程序中进行。上面的测试程序乍一看可能有点误导,因为我把写测试和读测试都塞进了同一个源代码中

不幸的是,我所处的实际情况是,我有一个文件,其中包含许多用writeObject编写的对象(没有reset),这将需要相当长的时间来重新生成(以天为单位)(而且reset会使输出文件变得庞大),所以如果可能的话,我希望避免这种情况。另一方面,我目前无法用readObject读取文件,即使堆空间已达到系统上可用的最大值

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

所以我的问题是:

  1. 到目前为止,我的所有研究都表明readUnshared的行为与对象的书写方式之间没有联系。这是怎么回事
  2. 考虑到数据是用writeObject和no reset写入的,有什么方法可以避免读取时的OOM吗

我不完全清楚为什么readUnshared无法解决这个问题

我希望这是清楚的。我这里空着,所以可能输入了奇怪的单词


comments开始回答以下问题:

If you're not calling writeObject() in the current instance of the JVM you should not be consuming memory by calling 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)
    
  • 这是一个video of it happening(在最近的测试程序编辑之前录制的视频,视频相当于新测试程序中的ReadMode.UNSHAREDWriteMode.NORMAL

  • 下面是some test data files,其中包含30000000个对象(压缩大小是一个小小的360KB,但请注意,它会扩展到一个惊人的2.34GB)。这里有四个测试文件,每个文件都是用writeObject/writeUnsharedreset的不同组合生成的。读取行为取决于它是如何写的,独立于readObjectreadUnshared。请注意writeObjectwriteUnshared数据文件是逐字节相同的,我无法确定这是否令人惊讶


我一直在盯着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,用于清除句柄表。我做了一个实验,每次阅读后我都称之为(通过反射),但它破坏了一切,所以这是不可能的


共 (2) 个答案

  1. # 2 楼答案

    However, it seems that if the objects were written using writeObject() rather than writeUnshared(), then readUnshared() does not decrease handle table usage.

    没错readUnshared()只会减少可归因于{}的句柄表的使用。如果您所在的JVM使用的是writeObject(),而不是writeUnshared(),那么writeObject()导致的句柄表使用率不会减少readUnshared()