Статьи

Адаптивный размер кучи

Одноразовые кофейные чашки Расширяя наш испытательный стенд для улучшения детектора проблем Plumbr GC , я написал небольшой тестовый пример, который, как мне показалось, может быть интересен для более широкой аудитории. Цель, которую я преследовал, состояла в том, чтобы проверить самоадаптивность JVM в отношении того, как сегментируется куча между eden, оставшимся в живых и постоянным пространством.

Сам тест генерирует объекты партиями. Пакеты создаются один раз в секунду, и каждый пакет имеет размер 500 КБ. На эти объекты ссылаются в течение пяти секунд, после чего ссылки удаляются, и объекты из этого конкретного пакета могут собираться мусором.

Тест был запущен с Oracle Hotspot 7 JVM в Mac OS X с использованием ParallelGC и ему предоставлено 30 МБ кучи для работы. Зная платформу, мы можем ожидать, что JVM запустится со следующей конфигурацией кучи:

  • JVM будет начинаться с 10 МБ в Young и 20 МБ в пространстве Tenured, поскольку без явной настройки JVM использует соотношение 1: 2 для распределения кучи между пространствами Young и Tenured.
  • В моей Mac OS X 10 МБ молодого пространства далее распределяется между Eden и двумя пространствами Выжившего, учитывая 8 МБ и 2×1 МБ соответственно. Опять же, это используемые по умолчанию платформы.

Действительно, когда мы запускаем тест и заглядываем под капот с помощью jstat , мы видим следующее, подтверждая наши оценки «за спиной»:

1
2
3
4
5
6
7
8
My Precious:gc-pressure me$ jstat -gc 2533 1s
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT  
1024.0 1024.0  0.0    0.0    8192.0   5154.4   20480.0      0.0     21504.0 2718.9      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   5502.1   20480.0      0.0     21504.0 2720.1      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   6197.5   20480.0      0.0     21504.0 2721.0      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   6545.2   20480.0      0.0     21504.0 2721.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   7066.8   20480.0      0.0     21504.0 2721.6      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   7588.3   20480.0      0.0     21504.0 2722.1      0    0.000   0      0.000    0.000

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

  • 8 МБ в Eden будут заполнены примерно за 16 секунд — помните, мы генерируем 500 КБ объектов в секунду
  • В каждый момент времени у нас есть приблизительно 2,5 МБ живых объектов — генерируя 500 КБ каждую секунду и сохраняя ссылки на объекты в течение пяти секунд, мы получаем примерно это число
  • Незначительный сборщик мусора срабатывает всякий раз, когда Eden заполнен — ​​это означает, что мы должны видеть второстепенный сборщик данных каждые 16 секунд или около того
  • После незначительного сборщика мусора мы закончим преждевременное повышение — места Survivor занимают всего 1 МБ, а живой набор объемом 2,5 МБ не поместится ни в одно из наших пространств 1 МБ Survivor. Таким образом, единственный способ очистить Eden — это распространение 1,5 МБ (2,5 МБ-1 МБ) живых объектов, не помещающихся в Survivor, в пространство Tenured.

Проверка журналов также дает нам уверенность в следующих прогнозах:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
My Precious:gc-pressure me$ jstat -gc -t 2575 1s
Time   S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT  
  6.6 1024.0 1024.0  0.0    0.0    8192.0   4117.9   20480.0      0.0     21504.0 2718.4      0    0.000   0      0.000    0.000
  7.6 1024.0 1024.0  0.0    0.0    8192.0   4639.4   20480.0      0.0     21504.0 2718.7      0    0.000   0      0.000    0.000
    ... cut for brevity ...
 14.7 1024.0 1024.0  0.0    0.0    8192.0   8192.0   20480.0      0.0     21504.0 2723.6      0    0.000   0      0.000    0.000
 15.6 1024.0 1024.0  0.0   1008.0  8192.0   963.4    20480.0     1858.7   21504.0 2726.5      1    0.003   0      0.000    0.003
 16.7 1024.0 1024.0  0.0   1008.0  8192.0   1475.6   20480.0     1858.7   21504.0 2728.4      1    0.003   0      0.000    0.003
    ... cut for brevity ...
 29.7 1024.0 1024.0  0.0   1008.0  8192.0   8163.4   20480.0     1858.7   21504.0 2732.3      1    0.003   0      0.000    0.003
 30.7 1024.0 1024.0 1008.0  0.0    8192.0   343.3    20480.0     3541.3   21504.0 2733.0      2    0.005   0      0.000    0.005
 31.8 1024.0 1024.0 1008.0  0.0    8192.0   952.1    20480.0     3541.3   21504.0 2733.0      2    0.005   0      0.000    0.005
    ... cut for brevity ...
 45.8 1024.0 1024.0 1008.0  0.0    8192.0   8013.5   20480.0     3541.3   21504.0 2745.5      2    0.005   0      0.000    0.005
 46.8 1024.0 1024.0  0.0   1024.0  8192.0   413.4    20480.0     5201.9   21504.0 2745.5      3    0.008   0      0.000    0.008
 47.8 1024.0 1024.0  0.0   1024.0  8192.0   961.3    20480.0     5201.9   21504.0 2745.5      3    0.008   0      0.000    0.008

Не через 16 секунд, а, скорее, примерно через каждые 15 секунд, запускается сборка мусора, очищается Eden и распространяется ~ 1 МБ живых объектов в одно из пространств Survivor и переполняется остальное в старое пространство.

Все идет нормально. JVM ведет себя так, как мы ожидаем. Интересная часть начинается после того, как JVM некоторое время контролирует поведение GC и начинает понимать, что происходит. Во время нашего теста это происходит примерно за 90 секунд:

01
02
03
04
05
06
07
08
09
10
11
12
My Precious:gc-pressure me$ jstat -gc -t 2575 1s
Time   S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT  
 94.0 1024.0 1024.0  0.0   1024.0  8192.0   8036.8   20480.0     8497.0   21504.0 2748.8      5    0.012   0      0.000    0.012
 95.0 1024.0 3072.0 1024.0  0.0    4096.0   353.3    20480.0    10149.6   21504.0 2748.8      6    0.014   0      0.000    0.014
 96.0 1024.0 3072.0 1024.0  0.0    4096.0   836.6    20480.0    10149.6   21504.0 2748.8      6    0.014   0      0.000    0.014
 97.0 1024.0 3072.0 1024.0  0.0    4096.0   1350.0   20480.0    10149.6   21504.0 2748.8      6    0.014   0      0.000    0.014
 98.0 1024.0 3072.0 1024.0  0.0    4096.0   1883.5   20480.0    10149.6   21504.0 2748.8      6    0.014   0      0.000    0.014
 99.0 1024.0 3072.0 1024.0  0.0    4096.0   2366.8   20480.0    10149.6   21504.0 2748.8      6    0.014   0      0.000    0.014
100.0 1024.0 3072.0 1024.0  0.0    4096.0   2890.2   20480.0    10149.6   21504.0 2748.8      6    0.014   0      0.000    0.014
101.0 1024.0 3072.0 1024.0  0.0    4096.0   3383.7   20480.0    10149.6   21504.0 2748.8      6    0.014   0      0.000    0.014
102.0 1024.0 3072.0 1024.0  0.0    4096.0   3909.7   20480.0    10149.6   21504.0 2748.8      6    0.014   0      0.000    0.014
103.0 3072.0 3072.0  0.0   2720.0  4096.0   323.0    20480.0    10269.6   21504.0 2748.9      7    0.016   0      0.000    0.016

Здесь мы видим удивительную адаптивность JVM. Узнав о поведении приложения, JVM изменила размер оставшегося в живых пространства, чтобы быть достаточно большой, чтобы вместить все живые объекты. Новая конфигурация для пространства Юнга теперь:

  • Eden 4MB
  • Survivor пространства 3MB каждый

После этого частота ГХ увеличивается — Eden теперь на 50% меньше, и вместо ~ 16 секунд он заполняется примерно за 8 секунд или около того. Но выгода также видна, поскольку места для выживших теперь достаточно велики, чтобы в них можно было разместить живые объекты в любой момент времени. В сочетании с тем фактом, что никакие объекты не живут дольше, чем один минорный цикл ГХ (помните, что в любой момент времени всего 2,5 МБ живых объектов), мы прекращаем продвижение объектов в старое пространство.

Продолжая следить за JVM, мы видим, что старое использование пространства остается неизменным после принятия. Больше нет объектов, распространяемых на старые, но поскольку не запускается ни один крупный сборщик мусора, ~ 10 МБ мусора, который удалось распространить до того, как произошла адаптация, будет жить в старом пространстве вечно.

Вы также можете отказаться от «удивительной адаптивности», если уверены в том, что делаете. Указание -XX-UseAdaptiveSizingPolicy в параметрах JVM заставит JVM придерживаться параметров, заданных во время запуска, и не пытаться перехитрить вас. Используйте эту опцию с осторожностью, современные JVM, как правило, действительно способны предсказать подходящую конфигурацию для вас.

Ссылка: Размер адаптивной кучи от нашего партнера JCG Никиты Сальникова Тарновского в блоге Plumbr Blog .