Всем привет, капнем сегодня немного глубже: в 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 = размер контейнера.