Как происходит сборка мусора в JVM
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как происходит сборка мусора в JVM
Garbage Collection (GC) — это автоматический механизм управления памятью в JVM, который выявляет и освобождает память, занимаемую объектами, которые больше не используются программой. GC позволяет разработчикам не беспокоиться об явном освобождении памяти (как в C/C++), но требует понимания принципов работы для оптимизации производительности.
Структура памяти JVM (Heap)
Heap подразделяется на регионы:
public class HeapStructureExample {
public static void main(String[] args) {
// Heap в Java подразделяется на:
// 1. Young Generation (молодое поколение)
// - Eden Space (80% молодого поколения)
// - Survivor Space 0 (10% молодого поколения)
// - Survivor Space 1 (10% молодого поколения)
// 2. Old Generation (старое поколение)
// 3. Permanent Generation / Metaspace (Java 8+)
// Команда для просмотра структуры heap:
// java -Xms1g -Xmx2g -XX:NewRatio=2 MyApp
// -Xms1g = начальный размер heap
// -Xmx2g = максимальный размер heap
// -XX:NewRatio=2 = отношение Young:Old = 1:2
}
}
Generational Hypothesis
Основной принцип GC - объекты часто умирают молодыми:
public class GenerationalHypothesis {
public static void main(String[] args) {
// Молодое поколение собирается часто, старое - редко
// Young Generation objects (часто удаляются)
for (int i = 0; i < 1_000_000; i++) {
String temp = new String("temporary");
// temp выходит из области видимости -> становится мусором
} // Minor GC происходит часто
// Old Generation objects (редко удаляются)
List<String> persistent = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
persistent.add(new String("persistent-" + i));
}
// persistent может жить долго -> Major GC редко трогает
}
}
Процесс сборки мусора
1. Minor GC (Молодое поколение)
Происходит часто (каждые несколько секунд).
Public class MinorGCExample {
public static void main(String[] args) throws InterruptedException {
// Фаза 1: Marking (разметка)
// GC определяет, какие объекты живы
List<String> alive = new ArrayList<>();
for (int i = 0; i < 100; i++) {
String temp = new String("garbage"); // Мусор
alive.add(new String("keep")); // Живой объект
}
// GC найдёт живые объекты
// Фаза 2: Sweeping (очистка)
// GC удаляет мёртвые объекты из Eden
// Фаза 3: Copying (копирование)
// Живые объекты копируются в Survivor Space
System.out.println("Minor GC завершена");
}
}
2. Major GC / Full GC (Полное поколение)
Происходит редко, может быть очень дорого.
public class MajorGCExample {
public static void main(String[] args) throws OutOfMemoryError {
List<Object> leakingObjects = new ArrayList<>();
try {
// Заполняем Old Generation
while (true) {
for (int i = 0; i < 1000; i++) {
byte[] largeArray = new byte[1024 * 100]; // 100KB
leakingObjects.add(largeArray);
// Объекты переживают Minor GC -> идут в Old
}
// Когда Old Generation почти полна
// -> Major GC (может занять секунды)
// -> Если памяти всё равно мало -> OutOfMemoryError
}
} catch (OutOfMemoryError e) {
System.out.println("Heap переполнена!");
}
}
}
Алгоритмы GC
1. Serial GC (Серийный)
Самый простой, использует один поток.
# Включение Serial GC
java -XX:+UseSerialGC MyApp
# Процесс:
# 1. Pause application (остановка приложения)
# 2. Mark-Sweep-Compact (маркировка, удаление, уплотнение)
# 3. Resume application
# Проблема: Stop-the-world pause может быть большим
public class SerialGCExample {
public static void main(String[] args) {
// Serial GC хорошо для small heap и single-threaded приложений
List<String> strings = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
strings.add(new String("Object " + i));
// Периодически происходит pause (неприемлемо для latency-sensitive)
}
}
}
2. Parallel GC (Параллельный)
Использует несколько потоков для GC.
# Включение Parallel GC (по умолчанию в Java 8)
java -XX:+UseParallelGC -XX:ParallelGCThreads=8 MyApp
# Процесс:
# 1. Pause application
# 2. Multiple threads выполняют marking и sweeping
# 3. Resume application
# Преимущество: Faster GC time
# Недостаток: Still has stop-the-world pause
3. CMS GC (Concurrent Mark-Sweep)
Выполняет маркировку параллельно с приложением.
# Включение CMS GC
java -XX:+UseConcMarkSweepGC MyApp
# Процесс:
# 1. Initial Mark (pause short) - отмечает root objects
# 2. Concurrent Mark (parallel) - маркирует живые объекты
# 3. Remark (pause short) - уточняет маркировку
# 4. Concurrent Sweep (parallel) - удаляет мусор
# Преимущество: Низкая latency
# Недостаток: Fragmentation, CPU использование
public class CMSGCBenefit {
public static void main(String[] args) throws InterruptedException {
// CMS позволяет приложению работать во время GC
Thread gcThread = new Thread(() -> {
System.out.println("CMS GC: Concurrent Mark-Sweep");
System.out.println("Приложение МОЖЕТ работать во время маркировки");
});
// Главная программа
for (int i = 0; i < 1000; i++) {
List<String> data = new ArrayList<>();
for (int j = 0; j < 100; j++) {
data.add(new String("data"));
}
Thread.sleep(10); // Приложение работает
}
}
}
4. G1GC (Garbage First)
Современный алгоритм для больших heap, по умолчанию в Java 10+.
# Включение G1GC
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 MyApp
# Процесс:
# 1. Heap разделена на множество small regions (обычно 2048)
# 2. GC приоритизирует regions с наибольшим мусором
# 3. Инкрементальные паузы вместо полных паузы
# Преимущество: Predictable pause times
# Рекомендуется для: Large heaps (> 4GB), latency-sensitive приложения
public class G1GCExample {
public static void main(String[] args) {
// G1GC оптимален для большие heap
List<byte[]> allocation = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
byte[] chunk = new byte[8192]; // 8KB chunks
allocation.add(chunk);
if (i % 10000 == 0) {
// G1 инкрементально собирает мусор
// Паузы предсказуемы (~200ms с настройкой выше)
}
}
}
}
Mark-Sweep-Compact алгоритм
Фазы работы GC:
public class MarkSweepCompactExample {
static class GCPhases {
// Фаза 1: MARK (маркировка)
// GC отмечает все живые объекты, начиная с GC roots
public void phase1Mark() {
// GC roots включают:
// - Local variables в stack frames
// - Static variables в классах
// - Objects referenced by threads
// DFS/BFS сканирует граф объектов
// Живые объекты отмечаются
}
// Фаза 2: SWEEP (удаление мусора)
public void phase2Sweep() {
// GC проходит по heap
// Неотмеченные объекты удаляются
// Фрагментация может увеличиться
}
// Фаза 3: COMPACT (уплотнение)
public void phase3Compact() {
// Живые объекты компактируются
// Фрагментация устраняется
// Может быть дорого
}
}
public static void main(String[] args) {
// Пример из исходного heap
// Heap: [obj1][free][obj2][obj3][free][free][obj4]
//
// После Mark: obj1, obj2, obj3, obj4 отмечены
//
// После Sweep: [obj1][obj2][obj3][obj4][free][free][free]
//
// После Compact: Живые объекты близко друг к другу
}
}
Отслеживание GC
Логирование GC:
# Включение GC logs
java -Xms1g -Xmx4g \
-Xlog:gc*:file=gc.log:time,level,tags \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
MyApp
Анализ в Java коде:
public class GCMonitoringExample {
public static void main(String[] args) {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gc : gcBeans) {
System.out.println("GC Name: " + gc.getName());
System.out.println("Collection Count: " + gc.getCollectionCount());
System.out.println("Collection Time: " + gc.getCollectionTime() + "ms");
}
// Мониторинг heap
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
System.out.println("Heap Usage: " + heapUsage.getUsed() / (1024 * 1024) + "MB");
}
}
Оптимизация GC
1. Настройка размеров heap:
# -Xms: начальный размер
# -Xmx: максимальный размер
# Обычно: -Xms = -Xmx для предсказуемости
java -Xms4g -Xmx4g MyApp
2. Выбор подходящего GC:
# Latency-sensitive приложения (Real-time)
java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 MyApp
# Throughput-focused приложения (Batch processing)
java -XX:+UseParallelGC -XX:ParallelGCThreads=16 MyApp
# Старые приложения (Java 8)
java -XX:+UseConcMarkSweepGC MyApp
3. Избежание утечек памяти:
public class MemoryLeakExample {
// ПЛОХО: Static collection без очистки
private static List<String> cache = new ArrayList<>();
public void addToCache(String key, String value) {
cache.add(key + ":" + value);
// cache растёт бесконечно -> OutOfMemoryError
}
// ХОРОШО: Ограниченный cache с eviction
private final Map<String, String> limitedCache =
new LinkedHashMap<String, String>(16, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 100; // Максимум 100 записей
}
};
}
4. Профилирование heap:
# Использование JVisualVM
jvisualvm &
# Или командная строка
jps -l # Список JVM процессов
jmap -histo <pid> # Heap histogram
jmap -dump:live,format=b,file=heap.bin <pid> # Dump heap
jhat heap.bin # Анализ dump
Заключение
Сборка мусора в JVM — это сложный процесс, включающий отслеживание живых объектов и освобождение памяти. Выбор правильного алгоритма GC критичен для производительности приложения. G1GC является рекомендуемым выбором для современных приложений, обеспечивая баланс между latency и throughput. Понимание принципов GC помогает разработчикам писать более эффективный код и избегать проблем с памятью.