Java. Менторинг за чашкой кода

@channel_java_cup_code
Всем привет, капнем сегодня немного глубже: в JVM и пройдем по ее памяти. Большинство знает, что JVM можно конфигурировать, используя различные флаги, но почему -Xmx2g не означает, что Java-процесс съест максимум 2 ГБ Часто при настройке Java-приложения мы пишем: -Xms2g -Xmx2g И возникает логичная мысль: «Отлично. JVM будет использовать максимум 2 ГБ памяти».И вот это совсем не так, -Xmx2g ограничивает только Java Heap. Процесс JVM состоит далеко не из одного Heap. Упрощённо память выглядит так: Java Process │ ├── Heap │ ├── Young Generation │ └── Old Generation │ ├── Metaspace ├── Thread Stacks ├── Code Cache ├── Direct Memory └── Native Memory Например, приложение действительно может иметь: -Xmx2g но при этом процесс спокойно занимать больше 2 ГБ. Почему? 1. Heap - здесь находятся обычные Java-объекты: User user = new User(); List<Order> orders = new ArrayList<>(); Именно размер Heap ограничивает -Xmx. 2. Metaspace - здесь JVM хранит метаданные загруженных классов. Например: class User { ... } Сам объект User находится в Heap, а информация о самом классе User — в Metaspace. Поэтому можно получить ситуацию: Heap → 2 GB Metaspace → 300 MB и процесс уже использует больше двух гигабайт. 3. Stack каждого потока - у каждого Java-потока есть собственный Stack. Например: -Xss1m Если приложение создало 500 потоков, потенциально это уже сотни мегабайт адресного пространства только под стеки. Именно поэтому «давайте создадим побольше потоков» — не бесплатная операция. 4. Direct Memory - например, Netty активно использует off-heap память. Она не обязана находиться в Java Heap: Heap ↓ Java objects Direct Memory ↓ native/off-heap buffers Поэтому приложение может иметь вполне нормальный Heap: Heap: 1.5 GB / 2 GB и при этом продолжать потреблять память за пределами этих 2 ГБ. Допустим, Kubernetes-контейнер имеет лимит: Memory limit = 2 GB А Java запускается с: -Xmx2g На первый взгляд всё прекрасно, но JVM получает возможность занять 2 ГБ только Heap, а ей ещё нужны Metaspace, стеки потоков, native memory, direct buffers и другие области. В результате: Container limit ↓ 2 GB │ ├── Heap 2 GB ├── Metaspace ├── Threads ├── Direct └── Native И приложение может получить: OOMKilled даже несмотря на то, что Heap ещё не достиг своего лимита, поэтому в production недостаточно сказать: «У нас Xmx = 2g, значит приложению нужно 2 ГБ памяти». Нужно смотреть на память всего процесса, и это одна из причин, почему настройка JVM — это не: -Xmx = размер контейнера.
Если у вас установлено приложение,
вы можете сразу перейти в канал