Сборщик мусора G1: Пермский ген заполняется неопределенно долго, пока не будет выполнен полный GC
У нас есть довольно большое приложение, работающее на сервере приложений JBoss 7. Раньше мы использовали ParallelGC, но это давало нам проблемы на некоторых серверах, где куча была большой (5 ГБ и более) и обычно почти заполнялась, мы часто получали очень большие GC-паузы.
В последнее время мы улучшили использование памяти приложения и в нескольких случаях добавили больше ОЗУ на некоторые из серверов, на которых работает приложение, но мы также начали переходить на G1 в надежде сделать эти паузы менее частыми и/или короче. Вещи, похоже, улучшились, но мы наблюдаем странное поведение, которого раньше не было (с ParallelGC): Пермский генерал, кажется, заполняется довольно быстро, и как только он достигает максимального значения, запускается Full GC, что обычно вызывает длительную паузу в потоках приложений (в некоторых случаях, более 1 минуты).
Мы используем 512 МБ максимального размера в течение нескольких месяцев, и во время нашего анализа размер perm обычно будет расти примерно на 390 МБ с помощью ParallelGC. Однако после того, как мы перешли на G1, началось поведение выше. Я пробовал увеличивать максимальный размер до 1 ГБ и даже 1,5 ГБ, но все же происходят полные GC (они менее редки).
В этой ссылке вы можете увидеть скриншоты используемого нами инструмента профилирования (YourKit Java Profiler). Обратите внимание, что при запуске Full GC у Eden и Old Gen есть много свободного места, но максимальный размер Perm. Размер Perm и количество загруженных классов значительно уменьшаются после Full GC, но они снова начинают расти, и цикл повторяется. Кэш кода хорош, никогда не поднимается выше 38 МБ (в этом случае он составляет 35 МБ).
Вот отрезок журнала GC:
2013-11-28T11:15: 57.774-0300: 64445.415: [Полный GC 2126M- > 670M (5120M), 23.6325510 secs] [Eden: 4096.0K (234.0M) → 0.0B (256.0M) Выжившие: 22.0M- > 0.0B Куча: 2126.1M (5120.0M) → 670.6M (5120.0M)] [Times: user = 10.16 sys = 0.59, real = 23.64 secs]
Вы можете увидеть полный журнал здесь (с момента запуска сервера, до нескольких минут после полного GC).
Вот информация о среде:
java version "1.7.0_45"
Java (TM) SE Runtime Environment (сборка 1.7.0_45-b18)
Java HotSpot (TM) 64-разрядная серверная VM (сборка 24.45-b08, смешанный режим)
Параметры запуска: -Xms5g -Xmx5g -Xss256k -XX:PermSize=1500M -XX:MaxPermSize=1500M -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -Xloggc:gc.log
Итак, вот мои вопросы:
-
Является ли это ожидаемым поведением с G1? Я нашел еще одно сообщение в Интернете о том, что кто-то спрашивает что-то очень похожее и говорит, что G1 должен выполнять инкрементные коллекции в Перми, но ответа не было...
-
Есть ли что-то, что я могу улучшить/исправить в наших параметрах запуска? Сервер имеет 8 ГБ оперативной памяти, но, похоже, нам не хватает аппаратного обеспечения, производительность приложения прекрасна до тех пор, пока не будет запущен полный GC, когда пользователи испытывают большие задержки и начинают жаловаться.
Ответы
Ответ 1
Причины роста Perm Gen
- Множество классов, особенно JSP.
- Множество статических переменных.
- Существует утечка загрузчика классов.
Для тех, кто не знает, вот простой способ подумать о том, как PremGen заполняется. У молодого генерала не хватает времени, чтобы все закончилось, и поэтому они переместились в пространство старого поколения. Пермский Ген имеет классы для объектов в Молодом и Ветхом Генеале. Когда объекты в Молодом или Старом Генеале собраны, а класс больше не ссылается, он получает "выгружен" из Пермского Генерала. Если Молодые и Молодые Старый генерал не получает GC'd, а затем и Пермский генерал, и как только он заполняется, ему нужен Full Stop-the-world GC. Для получения дополнительной информации см. Представление постоянного поколения.
Переход на CMS
Я знаю, что вы используете G1, но если вы переключитесь на сборщик низкоуровневых паролей Concurrent Mark Sweep (CMS) -XX:+UseConcMarkSweepGC
, попробуйте включить сбор классов и коллекцию постоянного поколения, добавив -XX:+CMSClassUnloadingEnabled
.
Скрытый Gotcha
Если вы используете JBoss, у RMI/DGC установлен gcInterval на 1 мин. Подсистема RMI заставляет полную сборку мусора раз в минуту. Это, в свою очередь, стимулирует продвижение, а не позволяет собирать его в Молодом поколении.
Вы должны изменить это, по крайней мере, на 1 час, если не 24 часа, чтобы GC выполнил правильные коллекции.
-Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000
Список всех опций JVM
Чтобы просмотреть все параметры, запустите это из строки cmd.
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version
Если вы хотите увидеть, что использует JBoss, вам нужно добавить следующее к вашему standalone.xml
. Вы получите список всех параметров JVM и того, для чего он установлен. ПРИМЕЧАНИЕ. Это должно быть в JVM, на который вы хотите посмотреть, чтобы использовать его. Если вы запустите его внешним, вы не увидите, что происходит в JVM, на котором работает JBoss.
set "JAVA_OPTS= -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal %JAVA_OPTS%"
Существует ярлык для использования, когда нас интересуют только измененные флаги.
-XX:+PrintcommandLineFlags
Диагностика
Используйте jmap, чтобы определить, какие классы потребляют пространство постоянного поколения. На выходе будет отображаться
Параметры JVM
Эти настройки работали для меня, но в зависимости от того, как ваша система настроена и что делает ваше приложение, определит, подходят ли они для вас.
-
-XX:SurvivorRatio=8
- Устанавливает отношение пространства выживших к 1: 8, в результате чего большие пространства для оставшихся в живых (чем меньше отношение, тем больше пространство). SurvivorRatio - это размер пространства Эдена по сравнению с одним оставшимся в живых. Большие оставшиеся в живых пространства позволяют коротким объектам более длительный период времени умирать в молодости.
-
-XX:TargetSurvivorRatio=90
- Позволяет заняться 90% оставшихся в живых, вместо 50% по умолчанию, что позволяет лучше использовать память памяти оставшегося в живых.
-
-XX:MaxTenuringThreshold=31
- Чтобы предотвратить преждевременное продвижение от молодого поколения к поколению. Позволяет короткоживущим объектам более длительный период времени умирать в молодом поколении (и, следовательно, избегать продвижения по службе). Следствием этого параметра является то, что незначительные GC-времена могут увеличиваться из-за дополнительных объектов для копирования. Это значение и размеры пространства для оставшихся в живых могут нуждаться в корректировке, чтобы сбалансировать накладные расходы на копирование между оставшимися в живых пространствами и объектами владения, которые будут жить в течение длительного времени. Настройки по умолчанию для CMS: SurvivorRatio = 1024 и MaxTenuringThreshold = 0, которые заставляют всех оставшихся в живых убирать. Это может оказать большое давление на единую параллельную нить, собирающую поколение. Примечание: при использовании с -XX: + UseBiasedLocking этот параметр должен быть равен 15.
-
-XX:NewSize=768m
- позволяет определять начальные размеры молодого поколения
-
-XX:MaxNewSize=768m
- разрешить спецификацию максимальных размеров молодого поколения
Ниже приведен более подробный список JVM options.
Ответ 2
Является ли это ожидаемым поведением с G1?
Я не считаю это удивительным. Основополагающим предположением является то, что материал, помещенный в пергень, почти никогда не становится мусором. Таким образом, вы ожидаете, что пермген GC будет "последним средством"; то есть что-то, что JVM будет делать только в том случае, если его принуждают к полному GC. (ОК, этот аргумент нигде не близок к доказательству... но он соответствует следующему.)
Я видел много доказательств того, что у других коллекционеров одинаковое поведение; например.
Я нашел еще одну публикацию в Интернете о том, что кто-то спрашивает что-то очень похожее и говорит, что G1 должен выполнять инкрементные коллекции в Перми, но ответа не было...
Я думаю, что нашел тот же пост. Но кто-то считает, что это должно быть возможно, не очень поучительно.
Есть ли что-то, что я могу улучшить/исправить в наших параметрах запуска?
Я сомневаюсь. Я понимаю, что это присуще стратегии GCG.
Я предлагаю вам либо отследить, либо исправить то, что использует столько перменов, в первую очередь... или переключиться на Java 8, в котором больше нет кучи permgen: см. Исключение PermGen в JDK 8
В то время как утечка пергентов является одним из возможных объяснений, есть другие; например.
- чрезмерное использование
String.intern()
,
- код приложения, который выполняет много динамического генерации класса; например используя
DynamicProxy
,
- огромная кодовая база... хотя это не вызовет перманентного оттока, как вы, кажется, наблюдаете.
Ответ 3
Сначала я попытался бы найти основную причину, по которой PermGen становится больше, до случайного использования JVM-параметров.
- Вы можете включить ведение журнала загрузки классов (-verbose: class, -XX: + TraceClassLoading -XX: + TraceClassUnloading,...) и вывести вывод
- В тестовой среде вы можете попробовать контролировать (над JMX), когда классы загружаются (java.lang: type = ClassLoading LoadedClassCount). Это может помочь вам узнать, какая часть вашего приложения несет ответственность.
- Вы также можете попробовать перечислить все классы с помощью инструментов JVM (извините, но я по-прежнему в основном использую jrockit и там вы будете делать это с помощью jrcmd. Hope Oracle перенес эти полезные функции в Hotspot...)
В общем, узнайте, что генерирует так много классов, а затем подумайте, как уменьшить это/настроить gc.
Cheers,
Димо
Ответ 4
Я согласен с ответом выше в том, что вам действительно нужно попытаться найти то, что на самом деле заполняет ваш пермг, и я бы сильно подозревал его в том, вы хотите найти основную причину.
Там этот поток на форумах JBoss, которые проходят через пару таких диагностированных случаев и как они были исправлены. этот ответ и в этой статье также обсуждается проблема. В этой статье упоминается, возможно, самый простой тест, который вы можете сделать:
Симптом
Это произойдет только при повторном развертывании вашего приложения без перезапуск сервера приложений. Серия JBoss 4.0.x пострадала от такой утечки загрузчика класса. В результате я не смог перераспределить наше приложение более двух раз, прежде чем JVM закончится Память и авария PermGen.
Решение
Чтобы определить такую утечку, разверните приложение и затем выполните полный дамп кучи (обязательно запустите GC перед этим). Затем проверьте, вы можете найти любой из ваших объектов приложения в дампе. Если так, следуйте их ссылкам на их корень, и вы найдете причину ваша утечка загрузчика. В случае JBoss 4.0 единственным решением было для перезапуска для каждого повторного развертывания.
Это то, что я постараюсь первым, ЕСЛИ вы думаете, что перераспределение может быть связано. Это сообщение в блоге является более ранним, делая то же самое, но также обсуждая детали. Основываясь на публикации, может быть, что вы на самом деле не перераспределяете что-либо, но перджен просто заполняется сам по себе. В этом случае рассмотрение классов + все, что добавлено в пермг, может быть способом (как уже упоминалось в предыдущем ответе).
Если это не даст больше понимания, мой следующий шаг будет проверять plumbr tool. У них есть своего рода гарантия на обнаружение утечки для вас.
Ответ 5
Вы должны запустить server.bat с командой java с помощью -verbose: gc