Я случайно обнаружил определение «Свинья в Python (примечание: это немного похоже на жадную и неадекватную змею, проглатывающую слона)» на китайском языке, когда я просматривал глоссарий по управлению памятью, поэтому я придумал эту статью. На первый взгляд, этот термин относится к тому, что GC постоянно продвигает крупные объекты из одного поколения в другое. Это похоже на то, как питон заглатывает свою добычу целиком, так что он не может двигаться, пока переваривает.
Следующие 24 часа мой разум был наполнен образами этого удушающего питона, от которого я не мог избавиться. Как говорят психиатры, лучший способ избавиться от страха — выговориться. Отсюда и эта статья. Но следующая история, о которой мы хотим поговорить, — это не питон, а настройка GC. Клянусь Богом.
Всем известно, что паузы в сборе мусора могут легко вызвать проблемы с производительностью. Современные JVM поставляются с продвинутыми сборщиками мусора, но, по моему опыту, найти оптимальную конфигурацию для определенного приложения крайне сложно. Ручная настройка, возможно, все еще дает надежду, но вы должны понимать точную механику алгоритма GC. В этом отношении вам будет полезна данная статья. Ниже я на примере объясню, как небольшое изменение в конфигурации JVM влияет на пропускную способность вашего приложения.
Пример
Приложение, которое мы использовали для демонстрации влияния GC на пропускную способность, представляло собой простую программу. Он содержит две темы:
PigEater Имитирует процесс поедания гигантским питоном большой жирной свиньи. Код делает это, добавляя 32 МБ байт в java.util.List и засыпая на 100 мс после каждого проглатывания.
PigDgester Имитирует процесс асинхронного пищеварения. Код, реализующий пищеварение, просто очищает список свиней. Поскольку это утомительный процесс, этот поток будет спать на 2000 мс каждый раз после очистки ссылки.
Оба потока будут выполняться в цикле while, поглощая и переваривая содержимое до тех пор, пока змея не наполнится. Для этого потребуется съесть около 5000 свиней.
Скопируйте код кода следующим образом:
пакет eu.plumbr.demo;
общественный класс PigInThePython {
статический изменчивый список свиней = новый ArrayList();
статическая изменчивость int pigsEaten = 0;
статический финал int ENOUGH_PIGS = 5000;
public static void main(String[] args) выдает InterruptedException {
новый PigEater().start();
новый PigDigester().start();
}
статический класс PigEater расширяет поток {
@Override
общественный недействительный запуск () {
в то время как (истина) {
pigs.add(новый байт[32 * 1024 * 1024]); //32 МБ на свинью
если (pigsEaten > ENOUGH_PIGS) возврат;
взятьАНап(100);
}
}
}
статический класс PigDigester расширяет поток {
@Override
общественный недействительный запуск () {
длинный старт = System.currentTimeMillis();
в то время как (истина) {
взятьАНап (2000);
pigsEaten+=pigs.size();
свиньи = новый ArrayList();
если (pigsEaten > ENOUGH_PIGS) {
System.out.format("Переварено %d свиней за %d мс.%n",pigsEaten, System.currentTimeMillis()-start);
возвращаться;
}
}
}
}
static void takeANap(int ms) {
пытаться {
Thread.sleep(мс);
} catch (Исключение е) {
е.printStackTrace();
}
}
}
Теперь мы определяем пропускную способность этой системы как «количество свиней, которые могут быть переварены в секунду». Учитывая, что в этот питон каждые 100мс запихивается свинья, мы видим, что теоретическая максимальная пропускная способность этой системы может достигать 10 свиней/секунду.
Пример конфигурации ГХ
Давайте посмотрим на производительность использования двух разных систем конфигурации. Независимо от конфигурации приложение работает на двухъядерном Mac (OS X10.9.3) с 8 ГБ оперативной памяти.
Первая конфигурация:
Куча 1,4 ГБ (-Xms4g -Xmx4g)
2. Используйте CMS для очистки старого поколения (-XX:+UseConcMarkSweepGC) и используйте параллельный сборщик для очистки нового поколения (-XX:+UseParNewGC).
3. Выделить 12,5% кучи (-Xmn512m) новому поколению и ограничить размеры области Эдема и области Выжившего одинаковыми.
Вторая конфигурация немного отличается:
Куча 1,2 ГБ (-Xms2g -Xms2g)
2. И новое, и старое поколение используют Parellel GC (-XX:+UseParallelGC).
3. Выделить 75% кучи новому поколению (-Xmn 1536m)
4. Теперь пришло время сделать ставку, какая конфигурация будет работать лучше (помните, сколько свиней можно съесть в секунду)? Те, кто ставил свои чипы на первую конфигурацию, будут разочарованы. Результат прямо противоположный:
1. Первая конфигурация (большая куча, большое старое поколение, CMS GC) может съедать 8,2 свиньи в секунду.
2. Вторая конфигурация (малая куча, большая нового поколения, Parellel GC) может съедать 9,2 свиньи в секунду.
Теперь давайте посмотрим на этот результат объективно. Выделяемых ресурсов в 2 раза меньше, но пропускная способность увеличивается на 12%. Это противоречит здравому смыслу, поэтому необходимо дополнительно проанализировать происходящее.
Анализ результатов ГХ
Причина на самом деле несложная. Ответ можно найти, внимательно взглянув на то, что делает GC при запуске теста. Здесь вы выбираете инструмент, который хотите использовать. С помощью jstat я открыл секрет. Команда, вероятно, такая:
Скопируйте код кода следующим образом:
jstat -gc -t -h20 PID 1 с
Анализируя данные, я заметил, что конфигурация 1 прошла 1129 циклов GC (YGCT_FGCT), что в общей сложности заняло 63,723 секунды:
Скопируйте код кода следующим образом:
Временная метка 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 секунды.
Скопируйте код кода следующим образом:
Временная метка 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 не находит долгоживущие объекты, он может быстрее очищать мусорные объекты. При первой конфигурации частота работы ГХ составит примерно 6–7 раз, а общее время паузы – 5–6 раз.
Рассказ этой истории преследует две цели. Прежде всего, мне хотелось выбросить из головы этого судорожного питона. Еще одно, более очевидное преимущество, заключается в том, что настройка GC — это очень умелый опыт, требующий от вас глубокого понимания основных концепций. Хотя приложение, используемое в этой статье, является очень распространенным приложением, различные результаты выбора также окажут большое влияние на планирование пропускной способности и мощности. В реальных приложениях разница здесь будет еще больше. Так что решать вам: можете ли вы освоить эти концепции или просто сосредоточиться на своей повседневной работе и позволить Plumbr подобрать наиболее подходящую конфигурацию ГХ для ваших нужд.