JEP draft: Prepare for Native Memory Tracking in the JDK
Подготовка к использованию Native Memory Tracking в JDK
| Автор | Johan Sjölén |
| Ответственный | Johan Sjölen |
| Тип | Feature |
| Область | JDK |
| Статус | Submitted |
| Компонент | hotspot / runtime |
| Обсуждение | core dash libs dash dev at openjdk dot org |
| Трудоёмкость | M |
| Длительность | M |
| Рецензенты | Alan Bateman, Dan Heidinga, Vladimir Kozlov |
| Создан | 2025/04/11 10:35 |
| Обновлён | 2026/07/30 13:49 |
| Задача | 8354416 |
Аннотация
Native Memory Tracking (NMT) — это функция JVM HotSpot, которая позволяет пользователю разделять выделения нативной памяти по категориям и помогает диагностировать проблемы с памятью с помощью отчётов. Мы расширяем NMT, чтобы выделения нативной памяти из базовых библиотек JDK можно было относить к отдельным категориям. Так пользователи смогут лучше диагностировать проблемы с нативной памятью.
Цели
- Заложить основу для перехода базовых библиотек JDK на NMT.
Что не является целью
- Перевод какой-либо конкретной базовой нативной библиотеки JDK на NMT не является целью. Интеграция в базовые библиотеки — задача дальнейшей работы.
Мотивация
JVM автоматически управляет памятью кучи для объектов с помощью сборки мусора. GC отслеживает каждый новый объект, чтобы освободить его память, когда объект станет недостижим. Благодаря GC Java-программистам не нужно явно отслеживать выделения памяти и жизненный цикл объектов, и поэтому целые классы ошибок, связанных с памятью, например утечки памяти, исключены by design (самой архитектурой).
Общее потребление памяти Java-приложением нельзя понять только по размеру кучи Java, потому что JVM также выделяет нативную память для своей работы и для хранения своих структур данных. Java-приложения, в том числе базовые библиотеки классов JDK, тоже выделяют нативную память через нативные библиотеки. Многие важные базовые библиотеки, например те, что обеспечивают файловый ввод-вывод, работу с сетью, сжатие и криптографию, используют нативную память. Для диагностики проблем с памятью в Java-программе полезно иметь полную картину общего потребления памяти программой, включая и кучу Java, и нативную память.
В экосистеме Java есть обширный набор инструментов для анализа и категоризации памяти кучи, но инструментов для анализа выделений нативной памяти гораздо меньше. Начиная с Java 8 в JVM есть подсистема NMT для отслеживания и анализа выделений нативной памяти самой JVM. NMT распределяет нативную память, используемую разными компонентами JVM, например метаданными классов, стеками потоков и кэшем кода, по категориям и сообщает текущий объём используемой памяти, а также число выделений и пиковое потребление памяти по категориям.
Разделяя эти сведения о нативной памяти по категориям и выводя их в отчётах, NMT может помочь разработчикам найти источник утечек нативной памяти и высокого потребления памяти со стороны JVM в их приложениях. Однако NMT доступен только JVM, поэтому все нативные выделения, выполняемые библиотеками классов Java в JDK, для NMT невидимы. К сожалению, из-за этого диагностировать проблемы с памятью в Java-приложениях сегодня сложнее, чем могло бы быть. Расширение NMT на базовые библиотеки JDK помогло бы решить эту проблему.
Описание
Мы расширяем NMT так, чтобы базовые библиотеки могли регистрировать собственные категории памяти и все их нативные выделения отслеживались в этих категориях. По мере интеграции базовых библиотек с NMT в отчётах jcmd и JFR будут появляться новые категории для потребления нативной памяти этими библиотеками JDK. Пользователям ничего менять не нужно: они просто лучше увидят, откуда берутся нативные выделения в их приложении.
То, что NMT сейчас не видит подробностей нативных выделений, выполняемых базовыми библиотеками, можно увидеть, если запустить с включённым NMT Java-программу, которая выполняет нативные выделения. Пример такой программы — HTTP-сервер из модуля jdk.httpserver. Внутри сервер использует NIO, а NIO выполняет нативные выделения. Поскольку запрос на выделение приходит из Java-программы, NMT правильно учтёт, что нативное выделение произошло, но не сможет правильно отнести его к категории. Если запустить следующую программу в JDK 25 с java -XX:NativeMemoryTracking=summary BasicHttpServer.java, можно получить отчёт NMT, в котором видна эта проблема.
import module java.base;
import module jdk.httpserver;
void main() throws IOException {
var port = 8000;
var server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/", new HelloHandler());
server.setExecutor(null);
var pid = ProcessHandle.current().pid();
IO.println("PID: " + pid);
IO.println("Server started at http://localhost:" + port);
server.start();
}
class HelloHandler implements HttpHandler {
public void handle(HttpExchange exchange) throws IOException {
var response = "hello world!";
exchange.sendResponseHeaders(200, response.length());
try (var os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
}
Запишите PID программы, и теперь в отдельном терминале можно запросить отчёт NMT.
term0 $ java -XX:NativeMemoryTracking=summary BasicHttpServer
PID: 12345
Server started at http://localhost:8000
term1 $ jcmd 12345 VM.native_memory summary
...
- Symbol (reserved=1379KB, committed=1379KB)
(malloc=923KB #3166) (at peak)
(arena=456KB #1) (at peak)
- Other (reserved=10KB, committed=10KB)
(malloc=10KB #2) (at peak)
...
В этом сильно сокращённом примере две категории: Symbol и Other. Категория Symbol содержит строки Java, которые VM интернировала. В ней 3166 выделений, сделанных malloc, и в сумме они занимают 923 КБ. Если для выделения категория не указана, оно попадает в категорию Other. В категории Other 10 КБ, выделенных за 2 выделения, но откуда взялись эти выделения, непонятно. Иными словами, NMT видит выделения, но не может правильно отнести их к категории. В этом примере можно предположить, что они сделаны HTTP-сервером, но в реальном приложении эта категория может заполняться памятью из разных источников и быть гораздо больше. Если мы сможем обеспечить, чтобы всем выделениям можно было указать категорию, то категория Other перестанет существовать и останутся только подробные категории нативных выделений.
После реализации этого JEP модуль HTTP-сервера сможет интегрироваться с NMT. После этого отчёт NMT будет показывать нативные выделения памяти HTTP-сервера в правильных категориях, а не в категории Other. Предположим, что HTTP-сервер использует одну категорию с именем httpserver. Тогда его потребление нативной памяти будет правильно учтено, как показано в сокращённом отчёте ниже:
$ java -XX:NativeMemoryTracking=summary BasicHttpServer.java &
[1] 12345
$ jcmd 12345 VM.native_memory summary
Native Memory Tracking:
(Omitting categories weighting less than 1KB)
Total: reserved=9760301KB, committed=614769KB
malloc: 24293KB #43040
mmap: reserved=9736008KB, committed=590476KB
- Code (malloc=264KB #2372) (peak=264KB #2373)
- Symbol (malloc=3442KB #26691) (at peak)
- httpserver (malloc=10KB #2) (at peak)
- Other (malloc=0KB #0) (at peak)
Дальнейшая работа
- Интегрировать соответствующие базовые библиотеки с NMT. Мы ожидаем, что эта работа будет выполняться постепенно.
- Рассмотреть расширение NMT на все библиотеки, а не только на библиотеки JDK.