← Назад к вопросам

Как распределяется память между потоками в 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

Важные правила

  1. Heap — общая, требует синхронизации
  2. Stack — приватный для каждого потока
  3. Volatile для флагов между потоками
  4. Synchronized для критических секций
  5. ThreadLocal для изоляции данных потока
  6. Atomic для атомарных операций
  7. Всегда очищай ThreadLocal.remove() в finally

Вывод

Память в JVM имеет четкое разделение: общая Heap память для всех потоков и локальная Stack память для каждого потока. Правильное управление доступом к общей памяти через синхронизацию — критически важно для написания корректных многопоточных приложений.

Как распределяется память между потоками в JVM | PrepBro