предисловие
Статья требует, чтобы читатель был знаком с общими принципами сборки мусора, встроенными в JVM. Память кучи делится на пространства Eden, Survivor и Tenured/Old, предположения о генерации и другие различные алгоритмы GC выходят за рамки этой статьи.
Minor GC
Восстановление памяти из пространства молодого поколения (включая регионы Eden и Survivor) называется Minor GC. Это определение ясно и легко для понимания. Однако, когда происходит событие Minor GC, следует отметить несколько интересных моментов:
1. Незначительный GC запускается, когда JVM не может выделить место для нового объекта, например, когда область Eden заполнена. Таким образом, чем выше скорость выделения, тем чаще выполняется второстепенная сборка мусора.
2. Когда пул памяти будет заполнен, все его содержимое будет скопировано, а указатель будет отслеживать свободную память с 0. Области Eden и Survivor помечаются и копируются, заменяя классические операции по маркировке, сканированию, сжатию и очистке. Так что фрагментации памяти в областях Эдем и Выживший нет. Указатель записи всегда остается в верхней части используемого пула памяти.
3. Постоянная генерация не будет затронута при выполнении операции Minor GC. Ссылки от постоянного поколения к молодому поколению обрабатываются как корни GC, а ссылки от молодого поколения к постоянному поколению напрямую игнорируются на этапе маркировки.
4. Вопреки общепринятому мнению, все второстепенные GC вызывают «остановку мира», останавливая потоки приложения. Для большинства приложений задержка, вызванная зависанием, незначительна. Правда в том, что большинство объектов в области Эдема можно считать мусором, и они никогда не будут скопированы в область Выжившего или пространство старого поколения. Если же наоборот, большинство новых объектов в районе Эдема не соответствуют условиям GC, и время паузы при выполнении Minor GC будет значительно больше.
Так что ситуация с Minor GC вполне ясна - каждый раз Minor GC будет очищать память молодого поколения.
Major GC vs Full GC
Следует отметить, что в настоящее время эти термины формально не определены ни в спецификации JVM, ни в научных статьях по сборке мусора. Но мы можем с первого взгляда увидеть, что эти определения верны, основываясь на том, что мы уже знаем.Незначительный сборщик мусора, очищающий молодую память, должен быть простым:
- Major GC должен очистить старое поколение.
- Полный сборщик мусора очищает все пространство кучи — как молодое, так и старое.
К сожалению, на самом деле все немного сложнее и запутаннее. Во-первых, многие основные GC вызываются второстепенными GC, поэтому во многих случаях невозможно разделить два GC. С другой стороны, многие современные механизмы сборки мусора очищают часть пространства постоянной генерации, поэтому использование термина «очистка» корректно лишь отчасти.
Это избавляет нас от необходимости заботиться о том, называется ли он основным сборщиком мусора или полным сборщиком мусора.Мы должны обратить внимание на то, останавливает ли текущий сборщик мусора все потоки приложения или может выполняться одновременно, не останавливая нити приложения.
Эта путаница встроена даже в стандартные инструменты JVM. Следующий пример очень хорошо объясняет, что я имею в виду. Давайте сравним результаты трассировки, выдаваемые двумя разными инструментами, сборщиком Concurrent Mark и Sweep (-XX:+UseConcMarkSweepGC), при работе в JVM.
Сначала попробуйте вывести через jstat:
my-precious: me$ jstat -gc -t 4235 1sTime 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.72010.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.81011.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.89612.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.97813.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.09114.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.23315.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.38616.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 второстепенных сборщиков мусора и 2 полных сборщика мусора выполнялись в течение 50 мс. Вы можете получить тот же результат с помощью инструментов на основе графического интерфейса, таких как jconsole или jvisualvm.
java -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC eu.plumbr.demo.GarbageProducer3.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]
Прежде чем кивнуть в знак согласия с этим выводом, давайте посмотрим на вывод журнала сборки мусора той же JVM, которая запустила сборку. Судя по всему - XX:+PrintGCDetails рассказывает нам другую и более подробную историю:
Основываясь на этой информации, мы видим, что после 12 минорных GC все начинает выглядеть по-другому. Вместо того, чтобы запускать полный сборщик мусора дважды, разница в том, что один сборщик мусора запускается дважды на разных этапах постоянного поколения:
1. Начальный этап маркировки занял 0,0041705 секунды, что составляет около 4 мс. На этом этапе событие «остановить мир» приостанавливается, останавливаются все потоки приложения и начинается маркировка.
2. Выполните этапы маркировки и очистки параллельно. Все они выполняются параллельно с потоками приложений.
3. На заключительном этапе Remark это заняло 0,0462010 секунд и около 46 мс. Эта фаза снова приостанавливает все события.
4. Параллельно выполнять операции по очистке. Как следует из названия, эта фаза также параллельна и не останавливает другие потоки.
Итак, как видно из журнала сборки мусора, на самом деле это просто основной сборщик мусора для очистки пространства старого поколения, а не полный сборщик мусора дважды.
Если вы примете решение позже, данные, предоставленные jstat, помогут вам принять правильное решение. В нем правильно перечислены два случая, когда все события приостанавливаются, что приводит к остановке всех потоков в общей сложности на 50 мс. Но если вы пытаетесь оптимизировать пропускную способность, вы будете введены в заблуждение. В листинге перечислены только начальный и заключительный этапы рециркуляции, а вывод jstat не показывает работу, выполненную одновременно.
в заключении
Учитывая эту ситуацию, лучше не думать в категориях Minor, Major, Full GC. Вместо этого отслеживайте задержку приложения или пропускную способность, а затем сопоставляйте события GC с результатами.
Когда происходят эти события GC, вам необходимо уделять особое внимание определенной информации, будь то событие GC, принудительно останавливающее все потоки приложения или обрабатывающее некоторые события параллельно.
Наконец
Прошу всех обратить внимание на мой паблик [Программист в погоне за ветром], в нем будут обновляться статьи, а также размещаться отсортированная информация.