Почему я получаю OutOfMemory, когда 20% кучи все еще бесплатно?

Я установил максимальную кучу до 8 ГБ. Когда моя программа начнет использовать около 6,4 ГБ (как указано в VisualVM), сборщик мусора начинает занимать большую часть процессора, а программа вылетает с OutOfMemory при распределении ~ 100 МБ. Я использую Oracle Java 1.7.0_21 для Windows.

Мой вопрос в том, есть ли варианты GC, которые помогут в этом. Я ничего не пропускаю, кроме -Xmx8g.

Мое предположение, что куча становится фрагментированной, но не должен ли GC компактно это делать?

Ответы

Ответ 1

Сбор битов и частей информации (что удивительно сложно, так как официальная документация неплохая), я определил...

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

Сборщик мусора CMS не выполняет уплотнения, в то время как остальные (серийные, параллельные, параллельные и G1). Сборщик по умолчанию в Java 8 - ParallelOld.

Все сборщики мусора разделяют память на регионы, и AFAIK, все они слишком ленивы, чтобы очень стараться предотвратить ошибку OOM. Вариант командной строки -XX:+PrintGCDetails очень полезен для некоторых из сборщиков, показывающих размеры регионов и сколько свободного места они имеют.

Можно экспериментировать с различными сборщиками мусора и настройками. Что касается моего вопроса, сборщик G1 (с флагом JVM -XX:+UseG1GC) решил проблему, с которой я столкнулся. Тем не менее, это было в основном до шанса (в других ситуациях, это OOM быстрее). Некоторые из сборщиков (серийный, cms и G1) имеют широкие возможности настройки для выбора размеров различных регионов, чтобы вы могли бесполезно тратить время, пытаясь решить проблему.

В конечном счете, реальные решения довольно неприятны. Во-первых, нужно установить больше оперативной памяти. Во-вторых, это использование меньших массивов. В-третьих, следует использовать ByteBuffer.allocateDirect. Прямые байтовые буферы (и их int/float/double wrappers) являются подобными массиву объектами с массивной производительностью, которые выделяются в рабочей куче операционной системы. Куча ОС использует аппаратное обеспечение виртуальной памяти процессора и не имеет проблем с фрагментацией и даже может эффективно использовать пространство подкачки (что позволяет выделять больше памяти, чем доступная оперативная память). Однако большой недостаток заключается в том, что JVM действительно не знает, когда следует освобождать прямые буферы, делая этот вариант более желательным для долгоживущих объектов. Конечным, возможно лучшим и, безусловно, самым неприятным вариантом является выделение и освобождение памяти изначально с помощью вызовов JNI и использование его в Java путем его обертки в ByteBuffer.

Ответ 2

Какой сборщик мусора вы используете? CMS не делает никакого уплотнения. Попробуйте использовать новый сборщик мусора G1 - это делает некоторое уплотнение.

Для небольшого контекста: сборщик мусора G1 или сборщик "Мусор первый" разбивает кучу на куски и после идентификации (маркировки) всего мусора он будет эвакуировать кусок, скопировав все живые биты в другой кусок - это то, что достигает уплотнения.

Для использования включите опцию -XX:+UseG1GC

Это дает отличное объяснение коллекции G1 и мусора в Java в целом.

Ответ 3

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

try {
    byte[] array = new byte[largeMemorySize];

} catch(OutOfMemroyError e) {
    System.out.printf("Failed to allocate %,d bytes, free memory= %,d%n", 
        largeMemorySize, Runtime.getRuntime().freeMemory());
    throw e;
}

Ответ 4

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

Изменить: насколько я знаю, вы не можете заставить Java упаковать память, чтобы вы могли использовать этот полный 8 ГБ, поскольку это потребовало бы, чтобы сборщик мусора должен был приостановить все остальные потоки, перемещая объекты, обновляя все ссылки на эти объекты, затем обновляя устаревшие записи кеша и т.д.... очень и очень дорогостоящую операцию.

Посмотрите на Фрагментация памяти

Ответ 5

-Xmx гарантирует, что куча не будет превышать 8 ГБ, но не гарантирует, что эта большая память будет фактически выделена. У вашей машины всего 8 ГБ памяти?