Статьи

Незначительный GC против Major GC против Full GC

Работая с функцией обнаружения паузы в GC в Plumbr, я был вынужден пробраться через множество статей, книг и презентаций по этому вопросу. На протяжении всего путешествия я неоднократно путался в (неправильном) использовании событий Minor, Major и Full GC. Это привело к этому сообщению в блоге, где я надеюсь, что мне удастся убрать часть этой путаницы.

В публикации предполагается, что читатель знаком с общими принципами сборки мусора, встроенными в JVM. Разделение кучи на пространства Eden, Survivor и Tenured / Old, гипотеза поколений и различные алгоритмы GC выходят за рамки этого поста.

минорная дс-мажорный-дс-полный дс

Незначительный GC

Сбор мусора из пространства Юнга (состоящего из пространств Эдема и Выжившего) называется Малой ГК . Это определение ясно и единообразно. Но есть все еще некоторые интересные приемы, которые вы должны знать при работе с событиями Minor Garbage Collection:

  1. Незначительный GC всегда запускается, когда JVM не может выделить место для нового объекта, например, Eden наполняется. Таким образом, чем выше коэффициент распределения, тем чаще исполняется Minor GC.
  2. Всякий раз, когда пул заполняется, все его содержимое копируется, и указатель может снова начать отслеживать свободную память с нуля. Таким образом, вместо классических Mark, Sweep и Compact очистка пространств Eden и Survivor выполняется с помощью Mark и Copy. Таким образом, в пространствах Эдема или Выжившего фактически не происходит фрагментации. Указатель записи всегда находится на вершине используемого пула.
  3. Во время незначительного события GC поколение Tenured фактически игнорируется. Ссылки из поколения в поколение считаются фактическими корнями GC. Ссылки от молодого поколения к постоянному поколению просто игнорируются на этапе разметки.
  4. Вопреки распространенному мнению, все второстепенные сборщики мусора запускают паузы остановки мира , останавливая потоки приложения. Для большинства применений длина пауз незначительна по времени ожидания. Это верно, если большинство объектов в Eden можно считать мусором и никогда не копировать в Survivor / Old space. Если верно обратное, и большинство новорожденных объектов не имеют права на GC, небольшие паузы GC начинают занимать значительно больше времени.

Так что с Minor GC ситуация была довольно ясной — каждый Minor GC чистит молодое поколение .

Major GC против Full GC

Следует отметить, что для этих терминов нет формальных определений. Ни в спецификации JVM, ни в исследовательских работах по сборке мусора. Но на первый взгляд построение этих определений поверх того, что мы знаем как правду о незначительной очистке ГХ. Пространство Юнга должно быть простым:

  • Major GC убирает Арендованное пространство.
  • Full GC очищает всю кучу — как молодых, так и постоянных.

К сожалению, это немного сложнее и запутаннее. Начнем с того, что многие крупные GC запускаются второстепенными GC, поэтому их разделение во многих случаях невозможно. С другой стороны — многие современные сборщики мусора выполняют уборку застроенного помещения частично, поэтому, опять же, использование термина «уборка» является лишь частично правильным.

Это приводит нас к тому, что вместо того, чтобы беспокоиться о том, называется ли GC Major или Full GC, следует сосредоточиться на том, чтобы выяснить, остановил ли GC все потоки приложения или он мог одновременно работать с потоками приложения .

Эта путаница даже встроена прямо в стандартные инструменты JVM. То, что я имею в виду, лучше всего объяснить на примере. Давайте сравним выходные данные двух разных инструментов, отслеживающих GC на JVM, работающей с Concurrent Mark и Sweep collector ( -XX: + UseConcMarkSweepGC )

Первая попытка получить представление через вывод jstat :

1
my-precious: me$ jstat -gc -t 4235 1s
01
02
03
04
05
06
07
08
09
10
11
12
13
Time S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT  
 5.7 34048.0 34048.0  0.0   34048.0 272640.0 194699.7 1756416.0   181419.9  18304.0 17865.1 2688.0 2497.6      3    0.275   0      0.000    0.275
 6.7 34048.0 34048.0 34048.0  0.0   272640.0 247555.4 1756416.0   263447.9  18816.0 18123.3 2688.0 2523.1      4    0.359   0      0.000    0.359
 7.7 34048.0 34048.0  0.0   34048.0 272640.0 257729.3 1756416.0   345109.8  19072.0 18396.6 2688.0 2550.3      5    0.451   0      0.000    0.451
 8.7 34048.0 34048.0 34048.0 34048.0 272640.0 272640.0 1756416.0  444982.5  19456.0 18681.3 2816.0 2575.8      7    0.550   0      0.000    0.550
 9.7 34048.0 34048.0 34046.7  0.0   272640.0 16777.0  1756416.0   587906.3  20096.0 19235.1 2944.0 2631.8      8    0.720   0      0.000    0.720
10.7 34048.0 34048.0  0.0   34046.2 272640.0 80171.6  1756416.0   664913.4  20352.0 19495.9 2944.0 2657.4      9    0.810   0      0.000    0.810
11.7 34048.0 34048.0 34048.0  0.0   272640.0 129480.8 1756416.0   745100.2  20608.0 19704.5 2944.0 2678.4     10    0.896   0      0.000    0.896
12.7 34048.0 34048.0  0.0   34046.6 272640.0 164070.7 1756416.0   822073.7  20992.0 19937.1 3072.0 2702.8     11    0.978   0      0.000    0.978
13.7 34048.0 34048.0 34048.0  0.0   272640.0 211949.9 1756416.0   897364.4  21248.0 20179.6 3072.0 2728.1     12    1.087   1      0.004    1.091
14.7 34048.0 34048.0  0.0   34047.1 272640.0 245801.5 1756416.0   597362.6  21504.0 20390.6 3072.0 2750.3     13    1.183   2      0.050    1.233
15.7 34048.0 34048.0  0.0   34048.0 272640.0 21474.1  1756416.0   757347.0  22012.0 20792.0 3200.0 2791.0     15    1.336   2      0.050    1.386
16.7 34048.0 34048.0 34047.0  0.0   272640.0 48378.0  1756416.0   838594.4  22268.0 21003.5 3200.0 2813.2     16    1.433   2      0.050    1.484

Этот фрагмент извлекается в течение первых 17 секунд после запуска JVM. Основываясь на этой информации, мы можем сделать вывод, что после 12 малых прогонов ГХ было выполнено два полных прогона ГХ, общей продолжительностью 50 мс . Вы получите то же самое подтверждение с помощью инструментов на основе графического интерфейса, таких как jconsole или jvisualvm .

Прежде чем кивнуть на этот вывод, давайте посмотрим на выходные данные журналов сборки мусора, собранных при том же запуске JVM. Очевидно -XX: + PrintGCDetails рассказывает нам другую и более подробную историю:

1
java -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC eu.plumbr.demo.GarbageProducer
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
3.157: [GC (Allocation Failure) 3.157: [ParNew: 272640K->34048K(306688K), 0.0844702 secs] 272640K->69574K(2063104K), 0.0845560 secs] [Times: user=0.23 sys=0.03, real=0.09 secs]
4.092: [GC (Allocation Failure) 4.092: [ParNew: 306688K->34048K(306688K), 0.1013723 secs] 342214K->136584K(2063104K), 0.1014307 secs] [Times: user=0.25 sys=0.05, real=0.10 secs]
... cut for brevity ...
11.292: [GC (Allocation Failure) 11.292: [ParNew: 306686K->34048K(306688K), 0.0857219 secs] 971599K->779148K(2063104K), 0.0857875 secs] [Times: user=0.26 sys=0.04, real=0.09 secs]
12.140: [GC (Allocation Failure) 12.140: [ParNew: 306688K->34046K(306688K), 0.0821774 secs] 1051788K->856120K(2063104K), 0.0822400 secs] [Times: user=0.25 sys=0.03, real=0.08 secs]
12.989: [GC (Allocation Failure) 12.989: [ParNew: 306686K->34048K(306688K), 0.1086667 secs] 1128760K->931412K(2063104K), 0.1087416 secs] [Times: user=0.24 sys=0.04, real=0.11 secs]
13.098: [GC (CMS Initial Mark) [1 CMS-initial-mark: 897364K(1756416K)] 936667K(2063104K), 0.0041705 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
13.102: [CMS-concurrent-mark-start]
13.341: [CMS-concurrent-mark: 0.238/0.238 secs] [Times: user=0.36 sys=0.01, real=0.24 secs]
13.341: [CMS-concurrent-preclean-start]
13.350: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
13.350: [CMS-concurrent-abortable-preclean-start]
13.878: [GC (Allocation Failure) 13.878: [ParNew: 306688K->34047K(306688K), 0.0960456 secs] 1204052K->1010638K(2063104K), 0.0961542 secs] [Times: user=0.29 sys=0.04, real=0.09 secs]
14.366: [CMS-concurrent-abortable-preclean: 0.917/1.016 secs] [Times: user=2.22 sys=0.07, real=1.01 secs]
14.366: [GC (CMS Final Remark) [YG occupancy: 182593 K (306688 K)]14.366: [Rescan (parallel) , 0.0291598 secs]14.395: [weak refs processing, 0.0000232 secs]14.395: [class unloading, 0.0117661 secs]14.407: [scrub symbol table, 0.0015323 secs]14.409: [scrub string table, 0.0003221 secs][1 CMS-remark: 976591K(1756416K)] 1159184K(2063104K), 0.0462010 secs] [Times: user=0.14 sys=0.00, real=0.05 secs]
14.412: [CMS-concurrent-sweep-start]
14.633: [CMS-concurrent-sweep: 0.221/0.221 secs] [Times: user=0.37 sys=0.00, real=0.22 secs]
14.633: [CMS-concurrent-reset-start]
14.636: [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Основываясь на этой информации, мы видим, что действительно после 12-ти минорных запусков ГК «что-то другое» начало происходить Но вместо двух прогонов Full GC эта «другая вещь» на самом деле представляла собой всего лишь один GC, работающий в поколении Tenured, состоящий из разных фаз:

  • Начальная фаза Mark, охватывающая 0,0041705 секунд или приблизительно 4 мс. Этот этап является событием остановки мира, останавливающим все потоки приложения для начальной разметки.
  • Параллельно выполняются фазы разметки и предчистоты. Они запускаются одновременно с потоками приложения
  • Фаза конечного замечания, охватывающая 0,0462010 секунд или приблизительно 46 мс. Эта фаза снова является событием остановки мира.
  • Одновременно выполняется операция развертки. Как следует из названия, этот этап также выполняется одновременно без остановки потоков приложения.

Итак, что мы видим из реальных журналов сбора мусора, так это то, что вместо двух операций Full GC была выполнена только одна основная очистка GC Старое пространство.

Если бы у вас были задержки, то принятие решений на основе данных, предоставленных jstat, привело бы вас к правильным решениям. Он правильно перечислил два события остановки мира общим объемом 50 мс, влияющих на задержку для всех активных потоков в тот самый момент. Но если бы вы пытались оптимизировать пропускную способность, вы бы ошиблись — перечисляя только начальные метки остановки и финальные метки замечаний, вывод jstat полностью скрывает выполняемую параллельную работу.

Вывод

Принимая во внимание ситуацию, лучше всего избегать мышления в терминах Minor, Major или Full GC. Вместо этого следите за задержкой или пропускной способностью своего приложения и связывайте события GC с результатами. Наряду с этими событиями вам необходимо иметь информацию о том, что конкретное событие GC принудительно остановило все потоки приложения или являлось частью события, обработанного одновременно.

Если вам понравился контент — это пример главы из нашего сборника мусора. Полное руководство будет выпущено в марте 2015 года.

Ссылка: Незначительные GC против Major GC против Full GC от нашего партнера JCG Никиты Сальникова Тарновского в блоге Plumbr Blog .