Поймать java.lang.OutOfMemoryError? - PullRequest
92 голосов
/ 21 апреля 2010

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

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

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

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

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

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

Ответы [ 14 ]

73 голосов
/ 21 апреля 2010

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

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

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

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

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

Демонстрация того, что 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) {} всюду или ерундой как этот.

29 голосов
/ 21 апреля 2010

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

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, как мы можем быть уверены, что обработчик перехвата не выделяет памяти сам по себе (какие-либо инструменты или лучшие практики)?

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

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

и затем установить его на ноль во время OOME:

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

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

14 голосов
/ 21 апреля 2010

В общем, пытаться перехватить и восстановить 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 «попробуй / поймай».)

13 голосов
/ 20 января 2011

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

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

9 голосов
/ 21 апреля 2010

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

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

Конечно, в большинстве случаев память истощается, и ситуация не может быть исправлена, но есть способы, которые имеют смысл.

7 голосов
/ 28 февраля 2012

Я столкнулся с этим вопросом, потому что мне было интересно, стоит ли ловить 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;
  }
}

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

5 голосов
/ 21 апреля 2010

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

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

См. Полный список здесь

4 голосов
/ 31 мая 2011

У меня есть приложение, которое необходимо восстанавливать после сбоев 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, который находится вне моего контроля. Я думал, что заранее выделил все необходимые ресурсы, но ясно, что аниматор выделяет память каждый раз, когда получает следующее изображение. Если у кого-то есть идея о том, как обращаться с ООМ, поднятыми в любой ветке , я хотел бы услышать.

2 голосов
/ 30 марта 2015

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

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

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

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

2 голосов
/ 25 июля 2012

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

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

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

...