Что происходит, когда недостаточно памяти для выброса ошибки OutOfMemoryError? - PullRequest
204 голосов
/ 13 февраля 2012

Мне известно, что каждый объект требует кучи памяти, а каждый примитив / ссылка в стеке требует стековой памяти.

Когда я пытаюсь создать объект в куче, и для этого недостаточно памяти, JVM создает java.lang.OutOfMemoryError в куче и выдает его мне.

Таким образом, это неявно означает, что JVM зарезервировала некоторую память при запуске.

Что происходит, когда эта зарезервированная память израсходована (она, безусловно, будет израсходована, см. Обсуждение ниже), и JVM не имеет достаточно памяти в куче для создания экземпляра java.lang.OutOfMemoryError

Это просто висит? Или он бросил бы мне null, так как нет new экземпляра OOM?

try {
    Object o = new Object();
    // and operations which require memory (well.. that's like everything)
} catch (java.lang.OutOfMemoryError e) {
    // JVM had insufficient memory to create an instance of java.lang.OutOfMemoryError to throw to us
    // what next? hangs here, stuck forever?
    // or would the machine decide to throw us a "null" ? (since it doesn't have memory to throw us anything more useful than a null)
    e.printStackTrace(); // e.printStackTrace() requires memory too.. =X
}

==

Почему JVM не может зарезервировать достаточно памяти?

Независимо от того, сколько памяти зарезервировано, эта память все еще может быть использована, если у JVM нет способа «восстановить» эту память:

try {
    Object o = new Object();
} catch (java.lang.OutOfMemoryError e) {
    // JVM had 100 units of "spare memory". 1 is used to create this OOM.
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        // JVM had 99 units of "spare memory". 1 is used to create this OOM.
        try {
            e.printStackTrace();
        } catch (java.lang.OutOfMemoryError e3) {
            // JVM had 98 units of "spare memory". 1 is used to create this OOM.
            try {
                e.printStackTrace();
            } catch (java.lang.OutOfMemoryError e4) {
                // JVM had 97 units of "spare memory". 1 is used to create this OOM.
                try {
                    e.printStackTrace();
                } catch (java.lang.OutOfMemoryError e5) {
                    // JVM had 96 units of "spare memory". 1 is used to create this OOM.
                    try {
                        e.printStackTrace();
                    } catch (java.lang.OutOfMemoryError e6) {
                        // JVM had 95 units of "spare memory". 1 is used to create this OOM.
                        e.printStackTrace();
                        //........the JVM can't have infinite reserved memory, he's going to run out in the end
                    }
                }
            }
        }
    }
}

Или более кратко:

private void OnOOM(java.lang.OutOfMemoryError e) {
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        OnOOM(e2);
    }
}

Ответы [ 11 ]

144 голосов
/ 13 февраля 2012

JVM никогда не хватает памяти. Он заранее вычисляет память из стека кучи.

Структура JVM, глава 3 , раздел 3.5.2, гласит:

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

Для Куча , раздел 3.5.3.

  • Если для вычисления требуется больше кучи, чем может быть доступно с помощью автоматической системы управления хранением, виртуальная машина Java бросает OutOfMemoryError.

Итак, он делает вычисления заранее, прежде чем делать выделение объекта.


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

Дополнительная литература. ? Кроме того, OutOfMemoryError может встречаться в другой структуре JVM.

64 голосов
/ 13 февраля 2012

Грэм Борланд, кажется, прав : по крайней мере моя JVM, по-видимому, повторно использует OutOfMemoryErrors.Чтобы проверить это, я написал простую тестовую программу:

class OOMTest {
    private static void test (OutOfMemoryError o) {
        try {
            for (int n = 1; true; n += n) {
                int[] foo = new int[n];
            }
        } catch (OutOfMemoryError e) {
            if (e == o)
                System.out.println("Got the same OutOfMemoryError twice: " + e);
            else test(e);
        }
    }
    public static void main (String[] args) {
        test(null);
    }
}

Запустив ее, вы получите такой вывод:

$ javac OOMTest.java && java -Xmx10m OOMTest 
Got the same OutOfMemoryError twice: java.lang.OutOfMemoryError: Java heap space

Кстати, JVM, на которой я работаю (на Ubuntu 10.04), это:

$ java -version
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)

Редактировать: Я пытался увидеть, что произойдет, если я заставлю JVM полностью запустить из памяти, используя следующую программу:

class OOMTest2 {
    private static void test (int n) {
        int[] foo;
        try {
            foo = new int[n];
            test(n * 2);
        }
        catch (OutOfMemoryError e) {
            test((n+1) / 2);
        }
    }
    public static void main (String[] args) {
        test(1);
    }
}

Как оказалось, кажется, что оно зациклено навсегда.Однако, что любопытно, попытка завершить программу с помощью Ctrl + C не работает, а выдает только следующее сообщение:

Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated

41 голосов
/ 13 февраля 2012

Большинство сред выполнения предварительно выделяют при запуске или иным образом резервируют достаточное количество памяти, чтобы справиться с ситуациями нехватки памяти. Я полагаю, что большинство здравомыслящих реализаций JVM сделали бы это.

23 голосов
/ 13 февраля 2012

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

12 голосов
/ 13 февраля 2012

Из спецификации JVM, глава 3.5.2:

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

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

Хотя это не должно гарантировать, что достаточно памяти, чтобы перехватить ее и напечатать хорошую трассировку стека ...

Добавление

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

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

11 голосов
/ 14 февраля 2012

Интересный вопрос :-).В то время как другие дали хорошие объяснения теоретических аспектов, я решил попробовать это.Это на Oracle JDK 1.6.0_26, Windows 7 64 бит.

Настройка теста

Я написал простую программу для исчерпания памяти (см. Ниже).

Программа просто создает статический java.util.List и продолжает вставлять в него свежие строки, пока не будет выдано OOM.Затем он ловит его и продолжает вставлять в бесконечный цикл (плохая JVM ...).

Результат теста

Как видно из результатов, первые четырераз OOME выбрасывается, он идет с трассировкой стека.После этого последующие OOME выдают java.lang.OutOfMemoryError: Java heap space, только если вызван printStackTrace().

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

Также интересен хэш-код OOME.Обратите внимание, что первые несколько OOME имеют разные хэши.Как только JVM начинает пропускать трассировки стека, хэш всегда одинаков.Это говорит о том, что JVM будет использовать свежие (предварительно выделенные?) Экземпляры OOME как можно дольше, но если push придет к пушу, он просто повторно использует тот же экземпляр, а не будет ничего бросать.

Вывод

Примечание. Я обрезал некоторые трассировки стека, чтобы сделать вывод более легким для чтения ("[...]").

iteration 0
iteration 100000
iteration 200000
iteration 300000
iteration 400000
iteration 500000
iteration 600000
iteration 700000
iteration 800000
iteration 900000
iteration 1000000
iteration 1100000
iteration 1200000
iteration 1300000
iteration 1400000
iteration 1500000
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1069480624
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.ArrayList.ensureCapacity(Unknown Source)
    at java.util.ArrayList.add(Unknown Source)
    at testsl.Div.gobbleUpMemory(Div.java:23)
    at testsl.Div.exhaustMemory(Div.java:12)
    at testsl.Div.main(Div.java:7)
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 616699029
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 2136955031
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1535562945
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
[...]

Программа

public class Div{
    static java.util.List<String> list = new java.util.ArrayList<String>();

    public static void main(String[] args) {
        exhaustMemory();
    }

    private static void exhaustMemory() {
        try {
            gobbleUpMemory();
        } catch (OutOfMemoryError e) {
            System.out.println("Ouch: " + e+"; hash: "+e.hashCode());
            e.printStackTrace();
            System.out.println("Keep on trying...");
            exhaustMemory();
        }
    }

    private static void gobbleUpMemory() {
        for (int i = 0; i < 10000000; i++) {
            list.add(new String("some random long string; use constructor to force new instance"));
            if (i % 10000000== 0) {
                System.out.println("iteration "+i);
            }
        }

    }
}
6 голосов
/ 13 февраля 2012

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

4 голосов
/ 17 февраля 2012

Чтобы дополнительно уточнить ответ @Graham Borland, функционально JVM делает это при запуске:

private static final OutOfMemoryError OOME = new OutOfMemoryError();

Позже JVM выполняет один из следующих байт-кодов Java: «новый», «anewarray» или «multianewarray». Эта инструкция заставляет JVM выполнить несколько шагов в состоянии нехватки памяти:

  1. Вызовите собственную функцию, скажем, allocate(). allocate() пытается выделить память для какого-то нового экземпляра определенного класса или массива.
  2. Этот запрос на выделение не выполняется, поэтому JVM вызывает другую нативную функцию, например, doGC(), которая пытается выполнить сборку мусора.
  3. Когда эта функция возвращается, allocate() пытается выделить память для экземпляра еще раз.
  4. Если это не удается (*), то JVM в allocate () просто выполняет throw OOME;, ссылаясь на OOME, который был создан при запуске. Обратите внимание, что ему не нужно выделять этот OOME, он просто ссылается на него.

Очевидно, это не буквальные шаги; в разных реализациях они будут различаться от JVM к JVM, но это идея высокого уровня.

(*) Значительный объем работы происходит здесь до отказа. JVM попытается очистить объекты SoftReference, попытаться выделить непосредственно в постоянное поколение при использовании сборщика поколений и, возможно, другие вещи, такие как финализация.

4 голосов
/ 14 февраля 2012

Исключения, указывающие на попытку нарушить границы среды управляемой памяти, обрабатываются средой выполнения указанной среды, в данном случае JVM. JVM - это собственный процесс, в котором выполняется IL вашего приложения. Если программа попытается сделать вызов, который расширяет стек вызовов за пределы или выделит больше памяти, чем может зарезервировать JVM, среда выполнения сама выдаст исключение, что приведет к отмене стека вызовов. Независимо от объема памяти, в которой ваша программа нуждается в данный момент, или глубины стека вызовов, JVM выделит достаточно памяти в пределах собственных границ процесса, чтобы создать указанное исключение и внедрить его в ваш код.

4 голосов
/ 14 февраля 2012

Вы, кажется, путаете виртуальную память, зарезервированную JVM, в которой JVM запускает программы Java с собственной памятью операционной системы, в которой JVM запускается как собственный процесс. JVM на вашем компьютере работает в памяти, управляемой ОС, а не в памяти, которую JVM зарезервировала для запуска программ Java.

Дополнительная литература:

И, наконец, попытка перехватить a java.lang.Error (и его потомков) для печати трассировки стека может не дать вам полезной информации. Вы хотите вместо этого кучу дампов.

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