Определите по выводу jstat, почему сервис проводит почти всё время в GC
Java-сервис перестаёт отвечать под обычной нагрузкой. Перезапуск помогает на несколько часов, затем симптом возвращается. Куча — -Xmx4g, конфигурацию приложения перед этим не меняли. jstat -gcutil <pid> 10s за пять минут печатает:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 12.44 38.10 97.82 95.1 91.3 412 6.905 18 41.230 48.135
0.00 9.87 44.02 98.61 95.1 91.3 414 6.941 26 61.884 68.825
0.00 11.03 51.77 99.04 95.1 91.3 415 6.955 34 82.117 89.072
0.00 10.15 60.31 99.31 95.1 91.3 416 6.960 43 104.556 111.516
Колонки: E — eden, O — old gen, M — metaspace (в % занятости); FGC/FGCT — число полных сборок и секунды.
Определите причину и назовите инструменты, которыми её подтвердите.
Занятость old gen O держится выше 97% и не падает после full GC, а FGC и FGCT растут круто — это идущие подряд полные сборки, которые почти ничего не освобождают. Признак удерживаемого живого набора (утечки), а не малого young gen. Подтверждают heap dump (jmap -dump), разобранный в Eclipse MAT или VisualVM, — его dominator tree называет удерживающий GC root, — плюс -Xlog:gc* или JFR для истории пауз.
- ✗Читать высокую занятость old gen как норму, а не как удерживаемый живой набор
- ✗Крутить флаги кучи, не подтвердив heap dump'ом, что именно удерживается
- ✗Считать растущее число полных сборок нормой для любого долгоживущего сервиса
- →Что dominator tree в Eclipse MAT скажет такого, чего не скажет
jstat? - →Как выглядел бы тот же вывод, если бы young gen действительно был мал?
Разбор
Что говорит вывод.
O(old gen) — 97.8 → 99.3%: занятость не падает после полных сборок. Значит, они не освобождают память.FGC18 → 43 за пять минут,FGCT41 → 104 с: сервис провёл в полных сборках больше минуты из пяти. Это и есть неотзывчивость — GC overhead.E(eden) растёт и обнуляется — young gen работает штатно; minor-сборки дёшевы (YGCTпочти не меняется).M(metaspace) стабилен — загрузчики классов ни при чём.
Вывод. Живой набор не помещается в old gen и продолжает расти: что-то удерживает объекты сильными ссылками. Классика — неограниченный кэш, статическая коллекция, слушатель, которого не сняли, ThreadLocal в пуле потоков.
Чем подтверждать.
jcmd <pid> GC.heap_dump /tmp/heap.hprof # снимок кучи (или jmap -dump:live,format=b,file=…)
jcmd <pid> GC.class_histogram # быстрая гистограмма: кто занимает байты
Дальше — Eclipse MAT (или VisualVM): dominator tree и Leak Suspects показывают не «кто большой», а кто удерживает — цепочку от GC root до объекта. Историю пауз даёт -Xlog:gc*:file=gc.log и Java Flight Recorder.
Правильный порядок: сначала найти удерживающий корень, только потом трогать флаги GC.