Оглавление:
Java под микроскопом: стек, куча и GC на примере кода
Схема - Java Memory Model - Heap / Non-Heap / Stack
Heap (память для объектов)
Создаёт объекты через
Young Generation: Eden + Survivor.
Old Generation: объекты, пережившие несколько сборок GC.
Размер Heap обычно больше Non-Heap.
new.
Young Generation: Eden + Survivor.
Old Generation: объекты, пережившие несколько сборок GC.
Размер Heap обычно больше Non-Heap.
Young Generation
Eden
Survivor
Old Generation
Non-Heap
Память, не связанная напрямую с объектами пользователя.
Включает Metaspace, Code Cache, Compressed Class Space, PermGen (для старых JVM).
Включает Metaspace, Code Cache, Compressed Class Space, PermGen (для старых JVM).
Metaspace
Static Area
Code Cache
Compressed Class Space
Stack (потоки)
Локальные переменные, параметры методов, адрес возврата.
Каждый поток JVM имеет свой стек.
Ссылки на объекты в Heap удерживают их живыми.
Каждый поток JVM имеет свой стек.
Ссылки на объекты в Heap удерживают их живыми.
Thread-1
Thread-2
Thread-3
Java Memory Deep Dive: как объекты, примитивы и методы живут в памяти
// Класс MemoryExample хранится в Metaspace
public class MemoryExample {
// Статическая переменная — хранится в Metaspace / Static area
static int staticVar = 10;
// Metaspace:
// MemoryExample.staticVar = 10
public static void main(String[] args) {
// Стек main:
// args -> ссылка на массив строк
System.out.println("Start of main");
// Создаём объект Point в куче (Eden), ссылка p хранится в стеке main
Point p = new Point(5, 7);
// Heap (Eden):
// [Point object header | x=5 | y=7]
// Стек main: p -> Point в куче
// Примитивная переменная a хранится в стеке main
int a = 20;
// Стек main: a = 20
// Вызов метода multiplyPoint с передачей ссылки p и примитива a
// JVM создаёт новый кадр стека для multiplyPoint
int result = multiplyPoint(p, a);
// ---------------- Кадр multiplyPoint ----------------
// pt -> ссылка на объект Point в куче
// factor = копия a (20)
// Операндный стек для вычислений
// Выполнение: return pt.x * pt.y * factor = 5 * 7 * 20 = 700
// ----------------------------------------------------
// После возврата из multiplyPoint кадр удаляется автоматически
// Стек main теперь снова активен
System.out.println("Result: " + result);
// Стек main: печатаем 700
// Обнуляем ссылку p
p = null;
// Heap: объект Point {x=5, y=7} теперь недостижим
// JVM может удалить объект автоматически, когда решит, что нужно освободить память
// Если объект пережил несколько GC → Survivor → Old Generation (предположительно)
// Создаём новый объект Point
Point q = new Point(10, 20);
// Heap (Eden):
// [Point object header | x=10 | y=20]
// Стек main: q -> Point в куче
System.out.println("End of main");
// После завершения main:
// Локальные переменные args, result, q очищаются
// Недостижимые объекты в куче будут собраны GC автоматически
}
static int multiplyPoint(Point pt, int factor) {
// ---------------- Кадр стека multiplyPoint ----------------
// pt -> ссылка на объект Point в куче
// factor = копия int
// Операндный стек JVM выполняет pt.x * pt.y * factor
// ----------------------------------------------------------
return pt.x * pt.y * factor;
}
}
// Класс Point хранится в Metaspace
class Point {
int x; // поле объекта — в куче
int y; // поле объекта — в куче
Point(int x, int y) {
// Кадр стека конструктора:
// параметры x, y — локальные копии
this.x = x; // присваиваем поле объекта в куче
this.y = y; // присваиваем поле объекта в куче
// После завершения конструктора кадр удаляется, объект остаётся в куче
}
@Override
public String toString() {
return "Point{" + x + "," + y + "}";
}
}
Java Memory Example: Пояснение
Этот пример показывает, как JVM распределяет объекты и переменные между стеком, кучей и статической областью, а также как объекты становятся кандидатами для сборки мусора (GC).
| Элемент | Где хранится | Комментарий |
|---|---|---|
| Локальные переменные метода (int a, ссылки на объекты) | Стек (Stack Frame) | Живут только внутри метода. После выхода из метода кадр удаляется. |
| Объекты (new Point(...)) | Heap (куча, Young Generation → Survivor → Old) | Создаются в Eden, могут перейти в Survivor и потом в Old, если переживут несколько сборок GC. |
| Статические переменные (staticVar) | Metaspace / Static area | Живут всю жизнь программы, GC их не трогает. |
| Объекты без ссылок (например, после p = null) | Heap (в Eden/Survivor/Old) | Недостижимые объекты становятся кандидатами для GC и будут удалены автоматически JVM. |
Метод multiplyPoint(pt, factor) создаёт кадр стека, который содержит:После return кадр удаляется, а результат возвращается в вызывающий метод.
- Ссылку pt на объект Point в куче
- Локальную переменную factor
- Операндный стек JVM для вычислений
Java Caching и GC: как SoftReference влияет на жизнь объектов в памяти
import java.lang.ref.SoftReference;
import java.util.HashMap;
public class CacheGCExample {
// Статическая кеш-карта — живёт в Metaspace/Static area
static HashMap<String, SoftReference<Point>> cache = new HashMap<>();
public static void main(String[] args) {
System.out.println("=== Start of main ===");
// Создаём обычный объект, ссылка хранится в стеке
Point p1 = new Point(100, 200);
// Heap (Eden): объект Point {x=100, y=200}
// Stack main: p1 -> объект Point в куче
// Добавляем объект в кеш через SoftReference
cache.put("first", new SoftReference<>(p1));
// Heap: объект SoftReference {ref -> Point (100,200)}
// Кеш хранится в Static area
// Даже если p1 станет null, объект может жить, пока SoftReference держит ссылку
// Убираем прямую ссылку
p1 = null;
// Heap: объект Point (100,200) всё ещё достижим через SoftReference
// Стек main: ссылка p1 удалена
// Вызываем метод, который создаёт временные объекты
createTemporaryPoints();
// Внутри метода кадр стека multiplyPoint: временные объекты удаляются после выхода
// Heap: объекты, на которые нет ссылок, candidate for GC
// Принудительный вызов сборщика мусора (только для демонстрации)
System.gc();
System.out.println("=== After System.gc() ===");
// Проверяем кеш
SoftReference<Point> ref = cache.get("first");
Point cachedPoint = ref.get(); // может вернуть null, если GC собрал объект
System.out.println("Cached point: " + cachedPoint);
System.out.println("=== End of main ===");
}
static void createTemporaryPoints() {
// Стек createTemporaryPoints: локальные ссылки temp1, temp2
Point temp1 = new Point(1, 2); // Heap: объект {x=1, y=2}
Point temp2 = new Point(3, 4); // Heap: объект {x=3, y=4}
// temp1, temp2 живут только в стеке метода
// После выхода из метода ссылки исчезнут -> объекты candidate for GC
}
}
// Класс Point хранится в Metaspace
class Point {
int x; // поле объекта — в куче, внутри блока объекта
int y;
Point(int x, int y) {
// Стек: параметры конструктора x,y
this.x = x; // присваиваем полю объекта в куче
this.y = y;
}
@Override
public String toString() {
return "Point{" + x + "," + y + "}";
}
}
Java SoftReference Cache Example: Пояснение
Этот пример демонстрирует использование SoftReference для кеширования объектов и работу сборщика мусора (GC). Объекты без прямых ссылок могут быть удалены, но пока на них есть SoftReference, JVM может их сохранить до нехватки памяти.Ключевые моменты
| Элемент | Где хранится | Комментарий |
|---|---|---|
| Локальные переменные метода (p1, temp1, temp2) | Стек (Stack Frame) | Живут только внутри метода. После выхода из метода кадр удаляется, ссылки исчезают. |
| Объекты Point (new Point(...)) | Heap (Eden → Survivor → Old, предположительно) | Создаются в Eden. Если на них нет ссылок (или осталась только SoftReference), GC может удалить объект при нехватке памяти. |
| SoftReference на объект | Heap + Static area (cache) | Объект сохраняется в памяти, пока JVM не решит собрать его при нехватке памяти. Статическая карта хранится в Metaspace/Static area. |
| Статическая кеш-карта (cache) | Metaspace / Static area | Живёт всю жизнь программы, GC не трогает карту напрямую, только объекты внутри неё. |
Метод createTemporaryPoints() создаёт локальные объекты Point (temp1, temp2) в стеке. После выхода из метода ссылки исчезают, и объекты становятся кандидатами для GC.
SoftReference позволяет “держать объект в памяти до нехватки памяти”, даже если прямых ссылок нет.
Gример с JVM, JIT и Code Cache
public class JITCodeCacheExample {
public static void main(String[] args) {
System.out.println("=== Start of main ===");
// Создаём объект, ссылка хранится в стеке main
Point p = new Point(2, 3);
// Heap: объект Point {x=2, y=3}
// Stack main: p -> Point
int result = 0;
// Цикл для разогрева JIT: JVM считает метод hot после нескольких вызовов
for (int i = 0; i < 1_000_000; i++) {
result += multiplyPoint(p, i);
// multiplyPoint вызывается многократно
// Сначала интерпретатор выполняет байткод
// Когда JVM считает метод hot → компилирует в Code Cache (нативный код)
// Дальнейшие вызовы идут сразу в Code Cache
}
System.out.println("Result: " + result);
// Обнуляем ссылку
p = null;
// Heap: объект Point становится кандидатом для GC
// Code Cache: multiplyPoint остаётся в памяти, GC не трогает Code Cache
System.out.println("=== End of main ===");
}
// Метод, который будет компилироваться в Code Cache после "разогрева"
static int multiplyPoint(Point pt, int factor) {
// Stack Frame создаётся при каждом вызове
return pt.x * pt.y * factor;
// Поля pt.x, pt.y читаются из Heap
// Инструкции самого метода после JIT лежат в Code Cache
}
}
// Класс Point хранится в Metaspace
class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Point{" + x + "," + y + "}";
}
}
Этот пример демонстрирует работу JIT-компилятора JVM: метод multiplyPoint после многократных вызовов компилируется в нативный код и хранится в Code Cache, а объекты и ссылки остаются в Heap/Stack.
Где что хранится
| Элемент | Где хранится | Комментарий |
|---|---|---|
Локальная переменная p |
Stack | Живёт пока кадр main активен |
Объект Point |
Heap (Eden → Survivor → Old) | Живёт пока есть достижимые ссылки, GC управляет удалением |
Метод multiplyPoint (байткод) |
Metaspace / Class Area | Живёт пока загружен класс |
Метод multiplyPoint (JIT нативный код) |
Code Cache | Живёт до освобождения Code Cache, GC на него не влияет |
| Цикл разогрева | Stack + Heap | Каждый вызов создаёт Stack Frame; объекты на Heap; после JIT последующие вызовы идут через Code Cache |
- Метод выполняется через интерпретатор, пока JVM не пометит его как hot.
- После многократных вызовов метод компилируется в Code Cache → суперскоростное выполнение.
- Code Cache отдельно от Heap, GC его не трогает.
- Объекты в Heap живут, пока есть достижимые ссылки, и могут быть собраны GC.
- Стековые кадры создаются на каждый вызов метода и удаляются после return.
Code Cache = "суперскоростной бинарный код JVM", Heap = объекты, Stack = кадры методов, Metaspace = байткод/классы.
Галерея
Мой канал в социальных сетях
Полезные статьи:
Java — Условные операторы Наглядная статья с примерами: if / else / логика / тернарный оператор / switch Кратко — условные операторы позволяют программе принимать решения: выполнить один кусок кода ...
Типы данных в Java Привет! С вами Виталий Лесных. В этом уроке курса «Основы Java для начинающих» разберем, что такое типы данных. Типы данных — это фундамент любого языка программирования. С их помо...
Введение Архитектура — это не просто способ расположить классы и модули. Это язык, на котором система разговаривает со временем. Сегодня Java-разработчик живёт в мире, где границы между сервисами, по...
Новые статьи:
Схема - Java Memory Model - Heap / Non-Heap / Stack Heap (память для объектов) Создаёт объекты через new. Young Generation: Eden + Survivor. Old Generation: объекты, пережившие несколько сборок G...
Признаки легаси-проекта: как распознать старый корабль Легаси — это не просто старый код. Это живой организм, который пережил десятки изменений, смену команд, устаревшие технологии и множество временн...
В современном Java-разработке есть три основных подхода к асинхронности и параллельности: CompletableFuture — для одиночных асинхронных задач. Flow / Reactive Streams — для потоков данных с контролем...