Вдохновение для этого поста пришло после того, как я наткнулся на определение « Свинья в Питоне » в глоссарии управления памятью . По-видимому, этот термин используется для объяснения ситуации, когда GC неоднократно продвигает крупные объекты из поколения в поколение. Эффект от этого, по-видимому, аналогичен эффекту питона, который проглатывает свою добычу целиком только для того, чтобы обездвижиться во время пищеварения.
В течение следующих 24 часов я просто не мог получить картину удушья питонов из моей головы. Как говорят психиатры, лучший способ избавиться от своих страхов — это рассказать о них. Итак, поехали. Но вместо питонов остальная часть истории будет о настройке сборки мусора. Я обещаю.
Остановки сбора мусора хорошо известны тем, что они могут стать узким местом в производительности. Современные JVM поставляются с современными сборщиками мусора, но, как я понял, найти оптимальную конфигурацию для конкретного приложения по-прежнему сложно. Чтобы даже иметь шанс вручную подойти к проблеме, нужно было бы понять точную механику алгоритмов сбора мусора. Этот пост может помочь вам в этом, так как я собираюсь использовать пример, чтобы продемонстрировать, как небольшие изменения в конфигурации JVM могут повлиять на пропускную способность вашего приложения.
пример
Приложение, которое мы используем для демонстрации влияния ГХ на пропускную способность, является простым. Он состоит всего из двух потоков:
- PigEater — имитирует ситуацию, когда питон продолжает есть одну свинью за другой. Код достигает этого путем добавления 32 МБ байтов в java.util.List и ожидания 100 мс после каждой попытки.
- PigDigester — имитирует процесс асинхронного переваривания. Код реализует пищеварение, просто обнуляя этот список свиней. Поскольку это довольно утомительный процесс, после каждой эталонной очистки эта нить спит в течение 2000 мс.
Оба потока будут работать в цикле while, продолжая есть и переваривать, пока змея не наполнится. Это происходит примерно при 5000 съеденных свиней.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
package eu.plumbr.demo; public class PigInThePython { static volatile List pigs = new ArrayList(); static volatile int pigsEaten = 0 ; static final int ENOUGH_PIGS = 5000 ; public static void main(String[] args) throws InterruptedException { new PigEater().start(); new PigDigester().start(); } static class PigEater extends Thread { @Override public void run() { while ( true ) { pigs.add( new byte [ 32 * 1024 * 1024 ]); //32MB per pig if (pigsEaten > ENOUGH_PIGS) return ; takeANap( 100 ); } } } static class PigDigester extends Thread { @Override public void run() { long start = System.currentTimeMillis(); while ( true ) { takeANap( 2000 ); pigsEaten+=pigs.size(); pigs = new ArrayList(); if (pigsEaten > ENOUGH_PIGS) { System.out.format( "Digested %d pigs in %d ms.%n" ,pigsEaten, System.currentTimeMillis()-start); return ; } } } } static void takeANap( int ms) { try { Thread.sleep(ms); } catch (Exception e) { e.printStackTrace(); } } } |
Теперь давайте определим пропускную способность этой системы как «количество переваренных свиней в секунду». Принимая во внимание, что свиньи вводятся в питона через каждые 100 мс, мы видим, что теоретическая максимальная пропускная способность этой системы может достигать до 10 свиней в секунду.
Настройка примера GC
Давайте посмотрим, как система ведет себя, используя две разные конфигурации. Во всех ситуациях приложение запускалось с использованием двухъядерного Mac (OS X 10.9.3) с 8G физической памяти.
Первая конфигурация:
- 4G кучи ( -Xms4g –Xmx4g )
- Использование CMS для очистки старого (-XX: + UseConcMarkSweepGC ) и Parallel для очистки молодого поколения -XX: + UseParNewGC )
- Выделил молодому поколению 12,5% кучи ( -Xmn512m ), еще больше ограничив размеры пространств Эдема и Выжившего равными по размеру.
Вторая конфигурация немного отличается:
- 2 г кучи ( -Xms2g –Xmx2g )
- Использование Parallel GC для сбора мусора как у молодого, так и у пожилого поколения ( -XX: + UseParallelGC )
- Выделил 75% кучи молодому поколению ( -Xmn1536m )
Теперь пришло время делать ставки, какая из конфигураций работала лучше с точки зрения пропускной способности (поросят, съедаемых в секунду, помните?). Те из вас, кто вкладывает деньги в первую конфигурацию, я должен вас разочаровать. Результаты в точности обратные:
- Первая конфигурация (большая куча, большое старое пространство, CMS GC) способна съесть 8,2 свиньи в секунду
- Вторая конфигурация (в 2 раза меньшая куча, большое молодое пространство, Parallel GC) способна съесть 9,2 свиньи в секунду
Теперь позвольте мне представить результаты в перспективе. Выделяя в 2 раза меньше ресурсов (по памяти), мы достигли на 12% лучшей пропускной способности . Это нечто настолько противоречащее общеизвестным фактам, что может потребоваться дополнительное разъяснение того, что на самом деле происходит.
Интерпретация результатов GC
Причина, по которой вы сталкиваетесь, не слишком сложна, и ответ смотрит прямо на вас, когда вы более внимательно смотрите на то, что GC делает во время теста. Для этого вы можете использовать инструмент по вашему выбору, который я заглянул под капот с помощью jstat, аналогично следующему :
jstat -gc -t -h20 PID 1с
Глядя на данные, я заметил, что первая конфигурация прошла 1129 циклов сбора мусора (YGCT + FGCT), что в общей сложности заняло 63,723 секунды:
1
2
3
4
5
|
Timestamp S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 594.0 174720.0 174720.0 163844.1 0.0 174848.0 131074.1 3670016.0 2621693.5 21248.0 2580.9 1006 63.182 116 0.236 63.419 595.0 174720.0 174720.0 163842.1 0.0 174848.0 65538.0 3670016.0 3047677.9 21248.0 2580.9 1008 63.310 117 0.236 63.546 596.1 174720.0 174720.0 98308.0 163842.1 174848.0 163844.2 3670016.0 491772.9 21248.0 2580.9 1010 63.354 118 0.240 63.595 597.0 174720.0 174720.0 0.0 163840.1 174848.0 131074.1 3670016.0 688380.1 21248.0 2580.9 1011 63.482 118 0.240 63.723 |
Вторая конфигурация остановилась в общей сложности 168 раз (YGCT + FGCT) всего на 11,409 секунд.
1
2
3
4
5
|
Timestamp S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 539.3 164352.0 164352.0 0.0 0.0 1211904.0 98306.0 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409 540.3 164352.0 164352.0 0.0 0.0 1211904.0 425986.2 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409 541.4 164352.0 164352.0 0.0 0.0 1211904.0 720900.4 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409 542.3 164352.0 164352.0 0.0 0.0 1211904.0 1015812.6 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409 |
Принимая во внимание, что работа, которую необходимо было выполнить в обоих случаях, была эквивалентна в том отношении, что — без видимых долгоживущих объектов, задача GC в этом упражнении по поеданию свиней состоит в том, чтобы просто избавиться от всего как можно быстрее. И используя первую конфигурацию, GC просто вынужден запускаться в ~ 6,7 раза чаще, что приводит к ~ 5,6-кратному увеличению общего времени паузы.
Таким образом, история выполнила две цели. Прежде всего, я надеюсь, что у меня в голове появилась картина удушливого питона. Другим и более важным выводом из этого является то, что настройка GC — это в лучшем случае сложное упражнение, требующее глубокого понимания нескольких основных концепций. Даже с действительно тривиальным приложением, использованным в этом сообщении в блоге, результаты, с которыми вы столкнетесь, могут оказать существенное влияние на планирование пропускной способности и емкости. В реальных приложениях различия еще более ошеломляющие. Так что выбор за вами, вы можете либо освоить концепции, либо сосредоточиться на своей повседневной работе и позволить Plumbr найти подходящую конфигурацию GC в соответствии с вашими потребностями.
Ссылка: | Сборка мусора: увеличение производительности от нашего партнера по JCG Никиты Сальникова Тарновского в блоге Plumbr Blog . |