Захват java.lang.OutOfMemoryError?

Документация для java.lang.Error гласит:

Ошибка - это подкласс Throwable, который указывает на серьезные проблемы, которые разумное приложение не должно пытаться уловить.

Но так как java.lang.Error является подклассом java.lang.Throwable, я могу поймать этот тип Throwable.

Я понимаю, почему не стоит ловить такого рода исключения. Насколько я понимаю, если мы решим перехватить его, обработчик перехвата не должен выделять какую-либо память самостоятельно. В противном случае OutOfMemoryError будет OutOfMemoryError снова.

Итак, мой вопрос:

  1. Существуют ли какие-либо сценарии реального мира при обнаружении java.lang.OutOfMemoryError может быть хорошей идеей?
  2. Если мы решим перехватить java.lang.OutOfMemoryError, как мы можем быть уверены, что обработчик перехвата не выделяет память самостоятельно (какие-либо инструменты или лучшие практики)?

Ответ 1

Я согласен и не согласен с большинством ответов здесь.

Существует несколько сценариев, в которых вы, возможно, захотите поймать OutOfMemoryError и по моему опыту (в Windows и Solaris JVM), очень редко находится OutOfMemoryError смертельный звон в JVM.

Есть только одна веская причина, чтобы поймать OutOfMemoryError, и это должно быть прекрасным, чистое освобождение ресурсов и протоколирование причины наилучшего возможного сбоя (если это все еще возможно).

В общем случае OutOfMemoryError происходит из-за выделения памяти блока, которое не может быть удовлетворено остальными ресурсами кучи.

Когда бросается Error, куча содержит такое же количество выделенных объектов, что и до неудачного выделения, и теперь настало время отказаться от ссылок на объекты во время выполнения, чтобы освободить еще больше памяти, которая может потребоваться для очистки. В этих случаях это может быть даже возможно продолжить, но это определенно будет плохой идеей, поскольку вы никогда не сможете быть на 100% уверены, что JVM находится в состоянии reparable.

Демонстрация того, что OutOfMemoryError не означает, что JVM не хватает памяти в блоке catch:

private static final int MEGABYTE = (1024*1024);
public static void runOutOfMemory() {
    MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
    for (int i=1; i <= 100; i++) {
        try {
            byte[] bytes = new byte[MEGABYTE*500];
        } catch (Exception e) {
            e.printStackTrace();
        } catch (OutOfMemoryError e) {
            MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
            long maxMemory = heapUsage.getMax() / MEGABYTE;
            long usedMemory = heapUsage.getUsed() / MEGABYTE;
            System.out.println(i+ " : Memory Use :" + usedMemory + "M/" + maxMemory + "M");
        }
    }
}

Вывод этого кода:

1 : Memory Use :0M/247M
..
..
..
98 : Memory Use :0M/247M
99 : Memory Use :0M/247M
100 : Memory Use :0M/247M

Если вы запускаете что-то критическое, я обычно ломаю Error, записываю его в syserr, а затем регистрирую его, используя мою систему ведения журналов по выбору, а затем отправляюсь освобождать ресурсы и закрываться чистым способом. Какое худшее может случиться? В любом случае JVM умирает (или уже мертв), и, поймав Error, есть хотя бы возможность очистки.

Предостережение заключается в том, что вы должны нацеливать ловушку этих типов ошибок только в местах, где возможна очистка. Не накрывайте catch(Throwable t) {} всюду или глупости.

Ответ 2

Вы можете восстановить его:

package com.stackoverflow.q2679330;

public class Test {

    public static void main(String... args) {
        int size = Integer.MAX_VALUE;
        int factor = 10;

        while (true) {
            try {
                System.out.println("Trying to allocate " + size + " bytes");
                byte[] bytes = new byte[size];
                System.out.println("Succeed!");
                break;
            } catch (OutOfMemoryError e) {
                System.out.println("OOME .. Trying again with 10x less");
                size /= factor;
            }
        }
    }

}

Но имеет ли это смысл? Что еще вы хотели бы сделать? Почему вы первоначально выделили большую часть памяти? Слишком мало памяти? Почему бы вам не использовать его в любом случае? Или, если это невозможно, почему бы не просто предоставить JVM больше памяти с самого начала?

Вернуться к вашим вопросам:

1: существуют ли какие-либо реальные сценарии слов при ловле java.lang.OutOfMemoryError может быть хорошей идеей?

Никто не приходит в голову.

2: если мы ловим java.lang.OutOfMemoryError, как мы можем убедиться, что обработчик catch не выделяет какую-либо память сам по себе (какие-либо инструменты или лучшие практики)?

Зависит от того, что вызвало OOME. Если он объявлен вне блока try, и это произошло поэтапно, ваши шансы немного. Вы можете заранее зарезервировать некоторое пространство памяти:

private static byte[] reserve = new byte[1024 * 1024]; // Reserves 1MB.

а затем установите значение 0 во время OOME:

} catch (OutOfMemoryException e) {
     reserve = new byte[0];
     // Ha! 1MB free!
}

Конечно, это делает все без особого смысла;) Просто дайте JVM достаточную память, как того требует ваше приложение. Запустите профайлер, если необходимо.

Ответ 3

В общем, это плохая идея, чтобы попытаться поймать и оправиться от OOM.

  1. OOME также может быть добавлен в другие потоки, включая потоки, о которых ваше приложение даже не знает. Любые такие потоки теперь будут мертвы, и все, что ожидает уведомления, может зависнуть навсегда. Короче говоря, ваше приложение может быть окончательно сломано.

  2. Даже если вы успешно восстановитесь, ваша JVM все еще страдает от голодной кучи, и в результате ваше приложение будет работать ужасно.

Лучшее, что можно сделать с OOME - это позволить JVM умереть.

(Это предполагает, что JVM действительно умирает. Например, OOM в потоке сервлета Tomcat не уничтожают JVM, и это приводит к переходу Tomcat в кататоническое состояние, когда он не отвечает ни на какие запросы... даже на запросы запустить снова.)

РЕДАКТИРОВАТЬ

Я не говорю, что это плохая идея поймать ООМ вообще. Проблемы возникают, когда вы затем пытаетесь восстановиться после OOME, намеренно или по недосмотру. Всякий раз, когда вы перехватываете OOM (напрямую или в качестве подтипа Error или Throwable), вы должны либо перебросить его, либо организовать выход из приложения /JVM.

В стороне: Это говорит о том, что для максимальной устойчивости перед лицом OOM приложение должно использовать Thread.setDefaultUncaughtExceptionHandler(), чтобы установить обработчик, который приведет к завершению приложения в случае OOME, независимо от того, в какой поток OOME создается. Мне было бы интересно мнение по этому поводу...

Единственный другой сценарий - это когда вы точно знаете, что ООМ не принесло никакого побочного ущерба; то есть вы знаете:

  • что конкретно вызвало ООМ,
  • что приложение делало в то время, и что можно просто отбросить это вычисление, и
  • что (грубо) одновременное OOME не могло произойти в другом потоке.

Существуют приложения, в которых можно узнать эти вещи, но для большинства приложений вы не можете точно знать, что продолжение после OOME безопасно. Даже если это эмпирически "работает", когда вы пытаетесь это сделать.

(Проблема в том, что требуется формальное доказательство того, что последствия "ожидаемых" OOME безопасны и что "непредвиденный" OOME не может возникнуть в рамках контроля OOME "попробуй/поймай".)

Ответ 4

Да, существуют реальные сценарии. Здесь мой: мне нужно обработать наборы данных из очень многих элементов в кластере с ограниченной памятью за node. Данные экземпляров JVM проходят через множество элементов один за другим, но некоторые из них слишком велики для обработки в кластере: я могу поймать OutOfMemoryError и принять к сведению, какие элементы слишком большие. Позже я могу повторно запускать только большие элементы на компьютере с большим объемом оперативной памяти.

(Поскольку это одноразрядное распределение массива, которое выходит из строя, JVM по-прежнему прекрасен после обнаружения ошибки и достаточно памяти для обработки других элементов.)

Ответ 5

Есть определенные сценарии, где ловить OOME имеет смысл. IDEA ловит их и открывает диалог, чтобы вы могли изменить настройки начальной загрузки (а затем выходите, когда закончите). Сервер приложений может заразиться и сообщить о них. Ключом к этому является сделать это на высоком уровне при отправке, чтобы у вас была разумная вероятность того, что куча ресурсов будет освобождена в тот момент, когда вы поймаете исключение.

Помимо вышеприведенного сценария IDEA, в общем случае ловушка должна быть Throwable, а не только OOM специально, и должна выполняться в контексте, в котором, по крайней мере, поток будет завершен в ближайшее время.

Конечно, в большинстве случаев память голодает, и ситуация не восстанавливается, но есть способы, чтобы это имело смысл.

Ответ 6

Я столкнулся с этим вопросом, потому что мне было интересно, есть ли хорошая идея поймать OutOfMemoryError в моем случае. Я частично отвечаю, чтобы показать еще один пример, когда поймать эту ошибку может иметь смысл для кого-то (то есть меня) и частично выяснить, действительно ли это в моем случае (со мной, будучи младшим разработчиком uber, я никогда не смогу быть слишком уверенным в какой-либо одной строке кода, которую я пишу).

В любом случае, я работаю над Android-приложением, которое можно запускать на разных устройствах с разными размерами памяти. Опасная часть - это декодирование растрового изображения из файла и удаление его в экземпляре ImageView. Я не хочу ограничивать более мощные устройства с точки зрения размера декодированного растрового изображения и не могу быть уверенным, что приложение не будет запущено на каком-то древнем устройстве, с которым я никогда не сталкивался с очень низкой памятью. Поэтому я делаю это:

BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); 
bitmapOptions.inSampleSize = 1;
boolean imageSet = false;
while (!imageSet) {
  try {
    image = BitmapFactory.decodeFile(filePath, bitmapOptions);
    imageView.setImageBitmap(image); 
    imageSet = true;
  }
  catch (OutOfMemoryError e) {
    bitmapOptions.inSampleSize *= 2;
  }
}

Таким образом, я могу обеспечить более и менее мощные устройства в соответствии с их, а точнее, потребностями и ожиданиями своих пользователей.

Ответ 7

Да, реальный вопрос - что вы собираетесь делать в обработчике исключений? Почти для всего полезного вы будете выделять больше памяти. Если вы хотите выполнить некоторую диагностическую работу при возникновении ошибки OutOfMemoryError, вы можете использовать -XX:OnOutOfMemoryError=<cmd> предоставляемую -XX:OnOutOfMemoryError=<cmd> HotSpot. Он выполнит ваши команды при возникновении ошибки OutOfMemoryError, и вы сможете сделать что-то полезное вне кучи Java. Вы действительно хотите не допустить исчерпания памяти приложением, поэтому первым делом выясните, почему это происходит. Затем вы можете соответствующим образом увеличить размер кучи MaxPermSize. Вот некоторые другие полезные хуки HotSpot:

-XX:+PrintCommandLineFlags
-XX:+PrintConcurrentLocks
-XX:+PrintClassHistogram

Смотрите полный список здесь

Ответ 8

У меня есть приложение, которое необходимо восстановить с ошибками OutOfMemoryError, и в однопоточных программах оно всегда работает, но иногда оно не работает в многопоточных программах. Приложение представляет собой автоматизированный инструмент тестирования Java, который выполняет сгенерированные тестовые последовательности до максимально возможной глубины на тестовых классах. Теперь пользовательский интерфейс должен быть стабильным, но в тестовом движке может не хватить памяти при выращивании дерева тестовых случаев. Я обрабатываю это с помощью следующего типа идиомы кода в тестовом движке:

boolean isOutOfMemory = false;  // flag used for reporting
try {
   SomeType largeVar;
   // Main loop that allocates more and more to largeVar
   // may terminate OK, or raise OutOfMemoryError
}
catch (OutOfMemoryError ex) {
   // largeVar is now out of scope, so is garbage
   System.gc();                // clean up largeVar data
   isOutOfMemory = true;       // flag available for use
}
// program tests flag to report recovery

Это работает каждый раз в однопоточных приложениях. Но недавно я включил свой тестовый движок в отдельный рабочий поток из пользовательского интерфейса. Теперь из памяти может произойти произвольно в любом потоке, и мне не ясно, как его поймать.

Например, у меня был OOME, когда кадры анимированного GIF в моем пользовательском интерфейсе циклически зависели от проприетарного потока, который создается за кадром классом Swing, который не под моим контролем. Я думал, что я выделил все ресурсы, необходимые заранее, но, очевидно, аниматор выделяет память каждый раз, когда он извлекает следующий образ. Если у кого-то есть представление о том, как обращаться с OOME, поднятыми в любом потоке, я бы с удовольствием услышал.

Ответ 9

Единственная причина, по которой я могу подумать, почему ловить ошибки OOM может быть то, что у вас есть несколько массивных структур данных, которые вы больше не используете, и можете установить нулевое значение и освободить некоторую память. Но (1) это означает, что вы теряете память, и вы должны исправить свой код, а не просто хромать вдоль OOME и (2), даже если вы его поймали, что бы вы сделали? OOM может произойти в любое время, потенциально оставив все наполовину выполненным.

Ответ 10

OOME можно поймать, но он будет вообще бесполезен, в зависимости от того, сможет ли JVM собирать мусор некоторые объекты, когда достигается захват, и сколько кучи памяти осталось к тому времени.

Пример: в моей JVM эта программа заканчивается:

import java.util.LinkedList;
import java.util.List;

public class OOMErrorTest {             
    public static void main(String[] args) {
        List<Long> ll = new LinkedList<Long>();

        try {
            long l = 0;
            while(true){
                ll.add(new Long(l++));
            }
        } catch(OutOfMemoryError oome){         
            System.out.println("Error catched!!");
        }
        System.out.println("Test finished");
    }  
}

Однако просто добавление отдельной строки в catch приведет к тому, что я говорю:

import java.util.LinkedList;
import java.util.List;

public class OOMErrorTest {             
    public static void main(String[] args) {
        List<Long> ll = new LinkedList<Long>();

        try {
            long l = 0;
            while(true){
                ll.add(new Long(l++));
            }
        } catch(OutOfMemoryError oome){         
            System.out.println("Error catched!!");
            System.out.println("size:" +ll.size());
        }
        System.out.println("Test finished");
    }
}

Первая программа работает нормально, потому что, когда достигается захват, JVM обнаруживает, что список больше не будет использоваться (это обнаружение также может быть оптимизировано во время компиляции). Поэтому, когда мы достигаем инструкции print, память кучи была полностью освобождена, поэтому у нас теперь есть большой запас маневра, чтобы продолжить. Это лучший случай.

Однако, если код организован, например, список ll используется после того, как OOME был уловлен, JVM не может его собрать. Это происходит во втором фрагменте. OOME, вызванный новым длинным созданием, улавливается, но вскоре мы создаем новый объект (строка в строке System.out,println), а куча почти заполнена, поэтому создается новый OOME. Это худший сценарий: мы пытались создать новый объект, мы потерпели неудачу, мы выбрали OOME, да, но теперь первая инструкция, требующая новой памяти кучи (например: создание нового объекта), выведет новый OOME. Подумайте об этом, что еще мы можем сделать в этот момент, так как осталось немного памяти?. Наверное, просто выходит. Следовательно, бесполезно.

Среди причин, по которым JVM не собирает ресурсы, действительно страшно: общий ресурс с другими потоками также использует его. Любой, у кого есть мозг, может увидеть, насколько опасным может быть заражение OOME, если оно вставлено в какое-то не экспериментальное приложение.

Я использую JVM 32bits для Windows x86 (JRE6). Память по умолчанию для каждого приложения Java составляет 64 МБ.

Ответ 11

Для вопроса 2 я уже вижу решение, которое я предложил бы BalusC.

  • Есть ли реальные сценарии слов при ловле java.lang.OutOfMemoryError может быть хорошей идеей?

Думаю, я просто натолкнулся на хороший пример. Когда awt-приложение отправляет сообщения, на stderr отображается uncatched OutOfMemoryError, и обработка текущего сообщения прекращается. Но приложение продолжает работать! Пользователь все еще может выдавать другие команды, не подозревая о серьезных проблемах, происходящих за сценой. Особенно, если он не может или не соблюдает стандартную ошибку. Таким образом, улавливание исключения oom и предоставление (или, по крайней мере, предполагающего) перезапуска приложения - это что-то желаемое.

Ответ 12

У меня просто сценарий, когда улавливание OutOfMemoryError, похоже, имеет смысл и, похоже, работает.

Сценарий: в Android-приложении я хочу отображать несколько растровых изображений с максимально возможным разрешением, и я хочу, чтобы они могли свободно их масштабировать.

Из-за быстрого масштабирования я хочу иметь растровые изображения в памяти. Однако у Android есть ограничения в памяти, которые зависят от устройства и которые трудно контролировать.

В этой ситуации может быть OutOfMemoryError при чтении растрового изображения. Здесь это помогает, если я поймаю его, а затем продолжу с более низким разрешением.

Ответ 13

  • Зависит от того, как вы определяете "хороший". Мы делаем это в нашем багги-веб-приложении, и он работает большую часть времени (к счастью, теперь OutOfMemory не происходит из-за несвязанного исправления). Однако, даже если вы его поймаете, все равно может сломать какой-то важный код: если у вас несколько потоков, распределение памяти может провалиться в любом из них. Таким образом, в зависимости от вашего приложения есть еще 10--90% вероятность его необратимого нарушения.
  • Насколько я понимаю, тяжелая стековая размотка в пути приведет к недействительности так много ссылок и, таким образом, освободит так много памяти, что вам это не нужно.

EDIT: Я предлагаю вам попробовать. Скажем, напишите программу, которая рекурсивно вызывает функцию, которая выделяет постепенно больше памяти. Поймайте OutOfMemoryError и посмотрите, сможете ли вы осмысленно продолжать с этой точки. По моему опыту, вы сможете, хотя в моем случае это произошло под сервером WebLogic, так что, возможно, была вовлечена какая-то черная магия.

Ответ 14

JVM предоставила полезные аргументы для работы с OutOfMemoryError. В этой статье мы хотели бы выделить эти аргументы JVM. Эти аргументы JVM:

  1. -XX: + HeapDumpOnOutOfMemoryError -XX: HeapDumpPath
  2. -XX: OnOutOfMemoryError
  3. -XX + ExitOnOutOfMemoryError
  4. -XX + CrashOnOutOfMemoryError

Когда вы передадите этот аргумент, JVM выйдет сразу после выброса OutOfMemoryError. Вы можете передать этот аргумент, если хотите закрыть приложение. Но лично я не предпочел бы настроить этот аргумент, потому что мы всегда должны стремиться к изящному выходу. Внезапный выход может поставить под угрозу транзакции, которые находятся в движении.

OutOfMemory связанные аргументы JVM

Ответ 15

Вы можете поймать что-либо под Throwable, вообще говоря, вы должны поймать подклассы Exception, исключая RuntimeException (хотя большая часть разработчиков также захватывает RuntimeException... но это никогда не было целью разработчиков языка).

Если бы вы поймали OutOfMemoryError, что бы вы сделали? У VM нет памяти, в основном все, что вы можете сделать, это выйти. Вероятно, вы даже не можете открыть диалоговое окно, чтобы сообщить им, что вы потеряли память, так как это займет память: -)

VM выдает OutOfMemoryError, когда она действительно не в памяти (действительно, все ошибки должны указывать на неустранимые ситуации), и вам действительно нечего делать, чтобы справиться с ней.

Что нужно сделать, это выяснить, почему у вас заканчивается память (используйте профилировщик, например, в NetBeans) и убедитесь, что у вас нет утечек памяти. Если у вас нет утечек памяти, увеличьте объем памяти, который вы выделите виртуальной машине.