Я недавно столкнулся с этим в каком-то коде - в основном кто-то пытается создать большой объект, справляясь, когда не хватает кучи для его создания:
try {
// try to perform an operation using a huge in-memory array
byte[] massiveArray = new byte[BIG_NUMBER];
} catch (OutOfMemoryError oome) {
// perform the operation in some slower but less
// memory intensive way...
}
Это кажется неправильным, поскольку сами Sun рекомендуют не пытаться поймать Error или его подклассы. Мы обсудили это, и возникла еще одна идея - явно проверить наличие свободной кучи:
if (Runtime.getRuntime().freeMemory() > SOME_MEMORY) {
// quick memory-intensive approach
} else {
// slower, less demanding approach
}
Опять же, это кажется неудовлетворительным - особенно в том, что выбор значения для SOME_MEMORY трудно связать с рассматриваемой задачей: для некоторого произвольного большого объекта, как я могу оценить, сколько памяти может потребоваться для его создания?
Есть ли лучший способ сделать это? Возможно ли это вообще в Java, или есть идея управления памятью ниже уровня абстракции самого языка?
Изменить 1: в первом примере, возможно, реально оценить объем памяти, который byte[] данной длины может занять, но есть ли более общий способ, который распространяется на произвольные большие объекты?
Изменить 2:, как указывает @erickson, есть способы оценить размер объекта после его создания, но (игнорируя статистический подход, основанный на предыдущих размерах объектов), есть ли способ сделать это для еще не созданных объектов?
Также, похоже, ведутся споры о том, разумно ли ловить OutOfMemoryError - кто-нибудь знает что-нибудь убедительное?




Я не верю, что есть разумный общий подход, который можно было бы смело считать 100% надежным. Даже подход Runtime.freeMemory уязвим из-за того, что у вас действительно может быть достаточно памяти после сборки мусора, но вы не узнаете об этом, если не вызовете gc. Но тогда нет и надежного способа принудительно запустить сборщик мусора. :)
Сказав это, я подозреваю, что если бы вы действительно знали, сколько вам нужно, и заранее запустили System.gc(), и вы работали в простом однопоточном приложении, у вас был бы достаточно приличный шанс получить это правильно с помощью. бесплатный вызов памяти.
Однако, если какое-либо из этих ограничений не срабатывает, и вы получаете ошибку OOM, вы вернетесь к исходной точке и, следовательно, вероятно, не лучше, чем просто перехватить подкласс Error. Хотя с этим связаны некоторые риски (виртуальная машина Sun не дает много гарантий относительно того, что происходит после OOM ... есть некоторый риск повреждения внутреннего состояния), существует множество приложений, для которых просто улавливают это и продолжают жить. не оставит вам серьезного вреда.
Однако у меня возникает более интересный вопрос: почему в одних случаях у вас достаточно памяти для этого, а в других - нет? Возможно, реальным ответом будет еще один анализ возможных компромиссов в производительности?
@jsight на тему «Почему бывают случаи, когда ..» А как насчет того, что вы (и я не говорю, что это должно выполняться в памяти) шифруете файл файловой системы. Иногда весь файл имеет размер 1 Мбайт, другие - 12 Гбайт. ? Когда это произойдет, невозможно освободить достаточно оперативной памяти !!
@Oscar: Я не думаю, что я бы попытался выяснить свободную память, чтобы определить, что делать с этими двумя случаями. Я с большей вероятностью буду основывать свое решение на размере файла с размером блока, определяемым на основе системной памяти при запуске, а не на основе телеметрии в реальном времени.
Определенно отловить ошибку - наихудший подход. Ошибка возникает, когда вы НИЧЕГО не можете с этим поделать. Даже не создать журнал, пыхтеть, типа «... Хьюстон, мы потеряли виртуальную машину».
Я не совсем понял вторую причину. Это было плохо, потому что трудно связать НЕКОТОРЫЕ ПАМЯТЬ с операциями? Не могли бы вы перефразировать это для меня?
Единственная альтернатива, которую я вижу, - это использовать жесткий диск в качестве памяти (RAM / ROM, как в старые времена). Я думаю, это то, на что вы указываете в своем «более медленном и менее требовательном подходе».
Каждая платформа имеет свои ограничения, поддержка java - столько, сколько ОЗУ ваше оборудование готово предоставить (ну, на самом деле, вы настраиваете виртуальную машину). В Sun JVM подразумевается, что это можно сделать с помощью
-Xmx
Вариант
нравиться
java -Xmx8g some.name.YourMemConsumingApp
Например
Конечно, вы можете попытаться выполнить операцию, занимающую 10 ГБ ОЗУ.
Если это ваш случай, вам обязательно нужно переключиться на диск.
Кроме того, использование шаблона стратегии может сделать код лучше. Хотя здесь это выглядит излишним:
if (isEnoughMemory(SOME_MEMORY)) {
strategy = new InMemoryStrategy();
} else {
strategy = new DiskStrategy();
}
strategy.performTheAction();
Но это может помочь, если "else" включает в себя много кода и плохо выглядит. Кроме того, если каким-то образом вы можете использовать третий подход (например, использование облака для обработки), вы можете добавить третью стратегию.
...
strategy = new ImaginaryCloudComputingStrategy();
...
:П
РЕДАКТИРОВАТЬ
После возникновения проблемы со вторым подходом: если бывают случаи, когда вы не знаете, сколько ОЗУ будет использовано, но знаете, сколько у вас осталось, вы можете использовать смешанный подход (ОЗУ, когда у вас достаточно , ROM [диск], когда вы этого не сделаете)
Допустим, это теоретическая проблема.
Предположим, вы получаете файл из потока и не знаете, насколько он велик.
Затем вы выполняете некоторую операцию с этим потоком (например, шифруете его).
Если вы используете только ОЗУ, это будет очень быстро, но если файл достаточно велик, чтобы занять всю память вашего приложения, вам нужно выполнить некоторые операции в памяти, а затем переключиться на файл и сохранить там временные данные.
При нехватке памяти виртуальная машина выполняет сборку мусора, вы получаете больше памяти, а затем выполняете другой фрагмент. И так повторяйте, пока не обработаете большой поток.
while( !isDone() ) {
if (isMemoryLow()) {
//Runtime.getRuntime().freeMemory() < SOME_MEMORY + some other validations
swapToDisk(); // and make sure resources are GC'able
}
byte [] array new byte[PREDEFINED_BUFFER_SIZE];
process( array );
process( array );
}
cleanUp();
freeMemory не совсем то. Вам также нужно будет добавить maxMemory () - totalMemory (). например предполагая, что вы запускаете виртуальную машину с max-memory = 100M, JVM может во время вызова вашего метода использовать только (из ОС) 50M. Из них, допустим, 30M фактически используются JVM. Это означает, что вы покажете 20 миллионов свободных (примерно, потому что мы говорим здесь только о куче), но если вы попытаетесь сделать ваш объект большего размера, он попытается захватить остальные 50 миллионов, которые его контракт позволяет ему взять из ОС, прежде чем сдаться и ошибиться. Таким образом, у вас на самом деле (теоретически) будет доступно 70M.
Чтобы сделать это более сложным, 30M, которые он сообщает, как использованные в приведенном выше примере, включают в себя вещи, которые могут подходить для сборки мусора. Таким образом, у вас может быть больше доступной памяти, если она достигнет потолка, он попытается запустить сборщик мусора, чтобы освободить больше памяти.
Вы можете попытаться обойти этот бит, вручную запустив System.GC, за исключением того, что это не так уж и хорошо, потому что
-не гарантируется немедленный запуск
-Он остановит все на своем пути, пока бежит
Лучше всего (при условии, что вы не можете легко переписать свой алгоритм для работы с меньшими фрагментами памяти, или записать в файл с отображением памяти, или что-то менее интенсивное по памяти), возможно, сделать безопасную приблизительную оценку необходимой памяти и убедиться, что он доступен до того, как вы запустите свою функцию.
System.gc () обычно не останавливает все. Это зависит от базового сборщика мусора.
Есть какие-то кладжи, которые можно использовать для оценки размера существующего объекта; вы можете адаптировать некоторые из них, чтобы предсказать размер еще не созданного объекта.
Однако в этом случае я думаю, что лучше всего поймать ошибку. Во-первых, запрос свободной памяти не учитывает то, что доступно после сборки мусора, которая будет выполняться перед вызовом OOME. И запрос на сборку мусора с помощью System.gc() ненадежен. Он часто явно отключается, потому что это может снизить производительность, а если он не отключен ... ну, он может снизить производительность при излишнем использовании.
От большинства ошибок невозможно избавиться. Однако возможность восстановления зависит от вызывающего, а не от вызываемого. В этом случае, если у вас есть стратегия восстановления после OutOfMemoryError, можно поймать его и отступить.
Думаю, на практике все сводится к разнице между «медленным» и «быстрым» способом. Если «медленный» метод достаточно быстр, я бы придерживался его, так как он безопаснее и проще. И, как мне кажется, разрешение использовать его как запасной вариант означает, что он «достаточно быстр». Не позволяйте небольшим оптимизациям подорвать надежность вашего приложения.
Подход «попытаться выделить и обработать ошибку» очень опасен.
Единственный жизнеспособный подход - это ваш второй подход с исправлениями, отмеченными в других ответах. Но вы должны быть уверены и оставить в куче лишнее «место для ненужных данных», когда решите использовать подход с интенсивным использованием памяти.
Вы столкнулись с реальной проблемой по голове - фактическое решение состояло в том, чтобы переписать метод так, чтобы ему не требовалось много памяти для начала. Вопрос был более академичным, но все же интересным ...