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