← Назад к вопросам
Как распределяется память между потоками в JVM
2.2 Middle🔥 191 комментариев
#JVM и управление памятью
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Как распределяется память между потоками в JVM
Архитектура памяти JVM
Память в JVM делится на две категории:
1. Shared Memory (Общая память для всех потоков)
Эта память доступна всем потокам и требует синхронизации для безопасного доступа:
Heap (Кучa)
- Основное место хранения объектов
- Управляется Garbage Collector
- Общая для всех потоков
- Может привести к выбросу OutOfMemoryError
Heap структура:
┌─────────────────────────────────┐
│ Young Generation │ (быстрый GC)
│ ┌──────────┬──────────┐ │
│ │ Eden │ Survivor │ │
│ └──────────┴──────────┘ │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Old Generation │ (долгоживущие объекты)
└─────────────────────────────────┘
Metaspace (PermGen в Java 8-)
- Хранит метаинформацию о классах
- Структура классов, методы, конструкторы
- Строковый пул интернированных строк
- Общая для всех потоков
2. Thread-Local Memory (Локальная память каждого потока)
Каждый поток имеет свою независимую память, на которую другие потоки не влияют:
Stack (Стек)
- Хранит локальные переменные и ссылки на объекты в heap
- Автоматически очищается при выходе из метода
- Размер обычно фиксирован (-Xss флаг)
- LIFO структура (Last In First Out)
- Выбрасывает StackOverflowError при переполнении
Каждый поток имеет свой Stack:
Поток 1 Stack Поток 2 Stack
┌──────────────┐ ┌──────────────┐
│ main() │ │ run() │
│ ├─ x = 5 │ │ ├─ y = 10 │
│ ├─ obj ref │ │ ├─ str ref │
│ └─ z = true │ │ └─ flag=false│
└──────────────┘ └──────────────┘
Program Counter (PC) Register
- Адрес текущей инструкции, выполняемой потоком
- Уникален для каждого потока
- Минимальный размер памяти
Native Method Stack
- Память для native (C/C++) методов
- Специфична для каждого потока
Практический пример
public class MemoryAllocationExample {
static int globalCounter = 0; // Metaspace
public static void main(String[] args) {
int localVar = 10; // Stack (main поток)
String name = new String("John"); // Heap, ref на Stack
Thread t1 = new Thread(() -> {
int threadLocalVar = 20; // Stack (t1)
String message = "Hello"; // Heap, ref на t1 Stack
globalCounter++; // Общее обновление в Heap/Metaspace
System.out.println(message);
});
Thread t2 = new Thread(() -> {
int threadLocalVar = 30; // Stack (t2)
String message = "World"; // Heap, ref на t2 Stack
globalCounter++; // Общее обновление в Heap/Metaspace
System.out.println(message);
});
t1.start();
t2.start();
}
}
Memory Visibility (Видимость памяти)
Между потоками есть проблема memory visibility — изменения в одном потоке могут не видны в другом.
Проблема: Data Race
public class DataRaceExample {
private int counter = 0; // Общая память
public void increment() {
counter++; // Race condition!
}
public void problematicCode() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) increment();
});
t1.start();
t2.start();
// Может быть разный результат: 1500-2000 вместо 2000
}
}
Решение 1: Synchronized
public class SynchronizedExample {
private int counter = 0;
public synchronized void increment() {
counter++; // Только один поток может выполнять
}
// Или явный lock
private final Object lock = new Object();
public void safeIncrement() {
synchronized(lock) {
counter++;
}
}
}
Решение 2: Volatile
public class VolatileExample {
private volatile boolean flag = false; // Всегда видна новая значение
public void setFlag(boolean value) {
flag = value; // Видно другим потокам
}
public boolean getFlag() {
return flag; // Всегда свежее значение
}
}
Volatile гарантирует:
- Memory visibility (видимость изменений между потоками)
- Правильный порядок операций
- НО не гарантирует atomicity (атомарность)
Решение 3: Atomic классы
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // Атомарная операция
}
public int getCounter() {
return counter.get();
}
}
Решение 4: Locks из java.util.concurrent
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int counter = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}
}
Thread-Local Storage
public class ThreadLocalExample {
// Каждый поток имеет свое значение
private static ThreadLocal<Connection> connectionHolder =
ThreadLocal.withInitial(() -> createConnection());
public static void setConnection(Connection conn) {
connectionHolder.set(conn);
}
public static Connection getConnection() {
return connectionHolder.get();
}
public static void cleanup() {
connectionHolder.remove(); // ВАЖНО: удалить, чтобы избежать утечки
}
}
JVM Memory Flags
# Размер Heap
-Xms1024m # Начальный размер heap (1 GB)
-Xmx2048m # Максимальный размер heap (2 GB)
# Размер Stack для каждого потока
-Xss256k # Stack size (по умолчанию 512-1024k)
# Metaspace
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
# Молодое поколение
-Xmn256m # Размер Young Generation
# Просмотр используемой памяти
java -XshowSettings:vm HelloWorld
Визуализация памяти при работе потоков
Main Heap (Shared)
┌──────────────────────────────────────────────┐
│ Object A │ Object B │ Object C │ Strings│
└──────────────────────────────────────────────┘
^ ^ ^
│ │ │
ref ref ref
│ │ │
Thread 1 Thread 2 Thread 3
Stack Stack Stack
┌──────┐ ┌──────┐ ┌──────┐
│ var1 │ │ var2 │ │ var3 │
│ ref→─┼─→│ ref→─┼────→ │ ref→─┼──┐
└──────┘ └──────┘ └──────┘ │
↓
Object B in Heap
Важные правила
- Heap — общая, требует синхронизации
- Stack — приватный для каждого потока
- Volatile для флагов между потоками
- Synchronized для критических секций
- ThreadLocal для изоляции данных потока
- Atomic для атомарных операций
- Всегда очищай ThreadLocal.remove() в finally
Вывод
Память в JVM имеет четкое разделение: общая Heap память для всех потоков и локальная Stack память для каждого потока. Правильное управление доступом к общей памяти через синхронизацию — критически важно для написания корректных многопоточных приложений.