Определите причину растущих stop-the-world (STW) пауз GC в проде
Торговый API работает на G1 с -Xmx16g -XX:MaxGCPauseMillis=50. p99-латентность была стабильной месяцами; после одного релиза она скачет до нескольких секунд. Объём трафика не менялся, а занятость кучи после каждой сборки медленно ползёт вверх. -Xlog:gc* показывает:
[3021.442s][info][gc] GC(812) Pause Young (Concurrent Start) (G1 Humongous Allocation) 14.2G->13.9G(16G) 62.418ms
[3024.905s][info][gc] GC(813) Pause Young (Normal) (G1 Evacuation Pause) 14.6G->14.4G(16G) 88.301ms
[3026.113s][info][gc] GC(814) To-space exhausted
[3026.113s][info][gc] GC(814) Pause Young (Normal) (G1 Evacuation Pause) 15.1G->15.0G(16G) 1204.772ms
[3029.550s][info][gc] GC(815) Pause Full (G1 Compaction Pause) 15.0G->12.7G(16G) 3841.006ms
[3033.884s][info][gc] GC(816) Pause Full (G1 Compaction Pause) 15.7G->13.1G(16G) 4102.559ms
Целевая пауза в 50 мс не выдерживается никогда.
Определите причину.
G1 Humongous Allocation помечает объекты крупнее половины региона: они выделяются сразу в old gen и собираются плохо. Они фрагментируют кучу, пока эвакуации не остаётся ни одного свободного региона (To-space exhausted), и G1 откатывается к полной stop-the-world компактизации — отсюда многосекундные паузы. Целевое время паузы не переживает откат к full GC. Правьте саму аллокацию, увеличьте -XX:G1HeapRegionSize и снизьте InitiatingHeapOccupancyPercent, чтобы конкурентный цикл стартовал раньше.
- ✗Читать
To-space exhaustedкак проблему размера survivor, а не нехватку свободных регионов - ✗Поднимать целевое время паузы вместо устранения слишком крупных аллокаций
- ✗Считать, что растущая после сборки занятость всегда означает обычную утечку
- →Почему объект крупнее половины региона
G1минует young gen? - →Избежал бы низколатентный
ZGCэтого сбоя, и какой ценой?
Разбор
Цепочка в логе читается сверху вниз.
G1 Humongous Allocation— приложение выделяет объекты крупнее половины регионаG1. Такой объект не проходит через eden: он кладётся сразу в old gen, занимая целиком один или несколько подряд идущих регионов. Хвост последнего региона пропадает впустую.- Куча фрагментируется: для очередного humongous-объекта нужен непрерывный блок регионов, а свободные регионы разбросаны.
To-space exhausted— эвакуации (копированию выживших) не нашлось свободного региона. Это не про размер survivor; это про то, что свободных регионов не осталось.Pause Full (G1 Compaction Pause)—G1откатывается к полной однопоточной-по-смыслу компактизации всей кучи. Это единственная фазаG1, которуюMaxGCPauseMillisне контролирует вообще. Отсюда 3.8 и 4.1 секунды.
Ключевой вывод. Целевое время паузы — это бюджет для обычных эвакуационных пауз. Как только G1 вынужден делать fallback full GC, цель перестаёт что-либо значить. Поднимать MaxGCPauseMillis бессмысленно: он не участвует в этом пути.
Что делать.
- Убрать аллокацию — она первопричина: релиз начал складывать ответ целиком в один
byte[]/ByteBuffer. Резать на куски, стримить, переиспользовать буферы. - Увеличить регион ⇒ меньше объектов считаются humongous.
- Опустить порог старта конкурентного цикла — он стартует раньше и успевает освободить регионы до исчерпания.
- Подтвердить диагноз по доле humongous-регионов.
# крупнее регион ⇒ меньше объектов проходят порог humongous (половина региона)
java -XX:G1HeapRegionSize=32m \
-XX:InitiatingHeapOccupancyPercent=35 \
-Xlog:gc+heap=debug:file=gc.log \
-Xmx16g -XX:MaxGCPauseMillis=50 -jar app.jar
# доля humongous-регионов в куче — подтверждение диагноза
jcmd <pid> GC.heap_info