Я бы в 5-гиговом JFR это не искал: быстрее посмотреть, как называются потоки. Пусть парсер поработает час, потом:
sudo jcmd $(pgrep -f zeon-backend) Thread.print \
| grep '^"' | sed -E 's/"([^"]*[^0-9-])[-0-9 ]*".*/\1/' \
| sort | uniq -c | sort -rn | head
Кто течёт, видно по имени: если сверху сотни "OkHttp ...", почти наверняка где-то на каждый запрос создаётся новый OkHttpClient (у каждого свой пул соединений и свой dispatcher), или не закрывается response.body у потоков радио. Лечится одним общим клиентом на всё приложение и use {} на ответе. Если сверху DefaultDispatcher-worker, значит что-то блокирующее висит внутри корутин.
Для такого я сделал себе MCP-сервер javaperf (
https://github.com/theSharque/mcp-jperf), я автор. Писал его, когда гонял нагрузочные тесты на своём бэкенде: он снимает дампы потоков, гистограммы кучи и JFR через jcmd и сразу их разбирает. Тогда он мне нашёл все горячие места. Тут пригодится analyze_threads: отдаёт потоки по состояниям и цепочки ожидания блокировок, можно снять дважды с интервалом и посмотреть, что прибавилось.
{ "mcpServers": { "javaperf": { "command": "npx", "args": ["-y", "javaperf"] } } }
Одна деталь: у вас java запущена через sudo, а jcmd и javaperf должны работать от того же пользователя, что и JVM.