Я запускаю OOM при чтении большого количества объектов из ObjectInputStream
с readUnshared
. MAT указывает на свою внутреннюю таблицу дескрипторов в качестве виновника, как и трассировка стека OOM (в конце этого сообщения). По всей видимости, это не должно происходить. Более того, независимо от того, происходит ли OOM, зависит от того, как были написаны ранее объекты.
В соответствии с эта запись по теме, readUnshared
должна решить проблему (в отличие от readObject
), а не создавая записи таблицы дескрипторов во время чтения (эта запись - это то, как я обнаружил writeUnshared
и readUnshared
, о которых я раньше не заметил).
Однако из моих собственных наблюдений видно, что readObject
и readUnshared
ведут себя одинаково и независимо от того, происходит ли OOM или нет, если объекты были написаны с помощью reset()
после каждой записи (неважно, было ли использовано writeObject
vs writeUnshared
, как я думал раньше - я просто устал, когда я впервые запускал тесты). То есть:
writeObject writeObject+reset writeUnshared writeUnshared+reset readObject OOM OK OOM OK readUnshared OOM OK OOM OK
Итак, действительно ли эффект readUnshared
действительно влияет на то, как он был написан. Это удивительно и неожиданно для меня. Я потратил некоторое время на трассировку 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
uncommented (иtestRead
not called) с высоким размером кучи, поэтомуwriteObject
не приведет к OOM. - Запустите программу тестирования второй раз с
testRead
uncommented (иtestWrite
not called) с размером кучи по умолчанию.
Чтобы быть ясным: я не занимаюсь написанием и чтением в одном экземпляре JVM. Мои записи происходят в отдельной программе из моих чтений. Вышеупомянутая тестовая программа может слегка вводить в заблуждение на первый взгляд из-за того, что я переполнял как тесты записи, так и чтения в один и тот же источник.
К сожалению, реальная ситуация, в которой я находилась, - это файл, содержащий много объектов, написанных с помощью writeObject
(без reset
), что потребуется некоторое время для восстановления (порядка дней) ( а также reset
делает выходные файлы массивными), поэтому я хотел бы избежать этого, если это возможно. С другой стороны, я не могу читать файл с помощью readObject
, даже если куча коснулась максимально доступной в моей системе.
Стоит отметить, что в моей реальной ситуации мне не нужно кэширование, предоставляемое таблицами дескрипторов потока объектов.
Итак, мои вопросы:
- Все мои исследования пока не показывают никакой связи между поведением
readUnshared
и тем, как были написаны объекты. Что здесь происходит? - Есть ли способ избежать чтения OOM при условии, что данные были написаны с помощью
writeObject
и noreset
?
Я не совсем уверен, почему readUnshared
не может решить проблему здесь.
Надеюсь, это ясно. Я бегу на пустом месте, поэтому, возможно, набрал странные слова.
Из комментариев в ответ ниже:
Если вы не вызываете
writeObject()
в текущем экземпляре JVM, вы не должны потреблять память, вызывая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)
-
Вот некоторые файлы тестовых данных, которые содержат 30 000 000 объектов (сжатый размер - это крошечный 360 КБ, но его предупреждают, он расширяется к колоссальному 2,34 ГБ). Здесь есть четыре тестовых файла, каждый из которых генерируется с различными комбинациями
writeObject
/writeUnshared
иreset
. Поведение чтения зависит только от того, как оно было написано и не зависит отreadObject
vs.readUnshared
. Обратите внимание, что файлы данныхwriteObject
vswriteUnshared
идентичны байтам для байта, я не могу решить, удивительно это или нет.
Я смотрел на ObjectInputStream
код отсюда. Мой текущий подозреваемый эта строка, представленная в 1.7 и 1.8:
ObjectStreamClass desc = readClassDesc(false);
Где этот параметр boolean
true
для unshared и false
для нормального. Во всех остальных случаях флаг "unshared" распространяется на другие вызовы, но в этом случае он жестко закодирован до false
, что приводит к добавлению дескрипторов в таблицу дескрипторов при чтении описаний классов для сериализованных объектов, даже когда readUnshared
используется. AFAICT, это единственный случай, когда флаг unshared не передается другим методам, поэтому я сосредоточен на нем.
Это отличается от, например, эта строка, где флаг unshared передается до readClassDesc
. (Вы можете проследить путь вызова от readUnshared
до обеих этих строк, если кто-то захочет в него копать.)
Тем не менее, я еще не подтвердил, что любое из этого является значительным или аргументировано, почему false
там жестко закодировано. Это всего лишь текущий трек, который я рассматриваю, это может оказаться бессмысленным.
Кроме того, fwiw, ObjectInputStream
имеет частный метод clear
, который очищает таблицу дескрипторов. Я сделал эксперимент, где я назвал это (через отражение) после каждого чтения, но он просто сломал все, так что не-go.