Статьи

OutOfMemoryError в переполненной куче

Почему я получаю OutOfMemoryError при выделении структуры данных, которая должна удачно вписаться в кучу, предоставленную для JVM? Это был вопрос, с которым я недавно столкнулся.

Действительно, при взгляде на то, что разработчик пытался выполнить, и тройной проверке размера кучи, предоставленной JVM через параметр -Xmx , действительно показалось, что происходит что-то неясное.

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

Как всегда, лучший способ понять проблему — это на собственном примере. Я построил небольшой синтетический тестовый пример:

1
2
3
4
5
6
package eu.plumbr.demo;
class ArraySize {
    public static void main(String... args) {
        int[] array = new int[1024*1024*1024];
    }
}

Код прост — все, что он пытается сделать, — это выделить массив из одного миллиарда элементов. Теперь, учитывая, что примитивам java int требуется 4 байта, можно подумать, что выполнение кода с кучей 6g будет работать нормально. В конце концов, эти миллиарды целых должны занимать только 4 г памяти. Итак, почему я вижу следующее, когда я выполняю код?

1
2
3
My Precious:bin me$ java –Xms6g –Xmx6g eu.plumbr.demo.ArraySize
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at eu.plumbr.demo.ArraySize.main(ArraySize.java:6)

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

Во-первых, для примитивов int в java действительно требуется 4 байта. Так что не похоже, что наша реализация JVM сошла с ума за ночь. И я могу заверить вас, что математика также верна — 1024 * 1024 * 1024 int примитивам действительно потребуется 4 294 967 296 байт или 4 гигабайта.

Чтобы понять, что происходит, давайте запустим тот же случай и включим ведение журнала сбора мусора, указав –XX: + PrintGCDetails:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
My Precious:bin me$ java –Xms6g -Xmx6g -XX:+PrintGCDetails eu.plumbr.demo.ArraySize
 
-- cut for brevity --
 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at eu.plumbr.demo.ArraySize.main(ArraySize.java:6)
 
Heap
 PSYoungGen      total 1835008K, used 125829K [0x0000000780000000, 0x0000000800000000, 0x0000000800000000)
  eden space 1572864K, 8% used [0x0000000780000000,0x0000000787ae15a8,0x00000007e0000000)
  from space 262144K, 0% used [0x00000007e0000000,0x00000007e0000000,0x00000007f0000000)
  to   space 262144K, 0% used [0x00000007f0000000,0x00000007f0000000,0x0000000800000000)
 ParOldGen       total 4194304K, used 229K [0x0000000680000000, 0x0000000780000000, 0x0000000780000000)
  object space 4194304K, 0% used [0x0000000680000000,0x0000000680039608,0x0000000780000000)
 PSPermGen       total 21504K, used 2589K [0x000000067ae00000, 0x000000067c300000, 0x0000000680000000)
  object space 21504K, 12% used [0x000000067ae00000,0x000000067b087668,0x000000067c300000)

Ответы теперь смотрят нам прямо в глаза: даже несмотря на то, что у нас достаточно общей кучи, ни одна отдельная область в куче не является достаточно большой, чтобы вместить 4 г объектов. Наша 6-граммовая куча разделена на четыре отдельные области, имеющие следующие размеры:

  • Эдем 1,536M
  • Пространства выживших ( от и до ) 256M каждый
  • OldGen 4,096M

Теперь, учитывая, что распределение объектов должно соответствовать одному региону, мы действительно можем видеть, что у приложения нет шансов — в любой из областей кучи недостаточно места для размещения этого единственного распределения 4g.

Итак, наша единственная надежда теперь увеличить кучу дальше? Даже если у нас уже есть избыточное обеспечение почти на 50% — передача 6 г кучи структуре данных, которая должна вписаться в 4 г? Не так быстро — есть альтернативное решение. Вы можете установить размер различных областей в памяти. Это не так просто и удобно для пользователя, как можно было ожидать, но две небольшие модификации конфигурации запуска помогут. При запуске одного и того же кода всего две дополнительные опции:

1
My Precious:bin me$ java -Xms6g -Xmx6g -XX:NewSize=5g -XX:SurvivorRatio=10 eu.plumbr.demo.ArraySize

тогда программа выполняет свою работу, и OutOfMemoryError не генерируется. Добавление -XX: + PrintGCDetails к автозагрузке также объясняет это:

1
2
3
4
5
6
7
8
9
Heap
 PSYoungGen      total 4806144K, used 4369080K [0x00000006c0000000, 0x0000000800000000, 0x0000000800000000)
  eden space 4369408K, 99% used [0x00000006c0000000,0x00000007caaae228,0x00000007cab00000)
  from space 436736K, 0% used [0x00000007e5580000,0x00000007e5580000,0x0000000800000000)
  to   space 436736K, 0% used [0x00000007cab00000,0x00000007cab00000,0x00000007e5580000)
 ParOldGen       total 1048576K, used 0K [0x0000000680000000, 0x00000006c0000000, 0x00000006c0000000)
  object space 1048576K, 0% used [0x0000000680000000,0x0000000680000000,0x00000006c0000000)
 PSPermGen       total 21504K, used 2563K [0x000000067ae00000, 0x000000067c300000, 0x0000000680000000)
  object space 21504K, 11% used [0x000000067ae00000,0x000000067b080c90,0x000000067c300000)

Мы видим, что размеры регионов сейчас действительно то, что мы просили:

  • Размер младшего целого (eden + два пространства выживших) равен 5g, как указано в параметре -XX: NewSize = 5g
  • Eden в 10 раз больше, чем выживший, как мы указали с параметром -XX: SurvivorRatio = 10 .

Обратите внимание, что в нашем случае оба параметра были необходимы. Если указать только -XX: NewSize = 5g, он все равно будет разделен между eden и оставшимися в живых так, что ни одна отдельная область не сможет удержать требуемые 4g.

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

Ссылка: OutOfMemoryError в переполненной куче от нашего партнера по JCG Владимира Сора в блоге Plumbr Blog .