1. Где хранятся локальные переменные
Начнём с самого простого: локальные переменные. Это переменные, которые объявляются внутри метода и существуют только во время его выполнения. Их жизнь коротка и драматична: как только метод завершён, все его локальные переменные исчезают без следа.
В Java локальные переменные хранятся в стеке. У каждого потока — свой собственный стек. Если метод вызывает другой метод, в стек добавляется новый «фрейм» (stack frame) с локальными переменными и адресом возврата. Когда метод завершает работу, его фрейм убирается со стека.
Пример: жизнь локальной переменной
public class LocalVariableDemo {
public static void main(String[] args) {
int a = 42; // локальная переменная a живёт только в main
printSquare(a);
// Здесь переменной b уже нет!
}
public static void printSquare(int b) {
int square = b * b; // локальная переменная square
System.out.println("Квадрат: " + square);
// После выхода из printSquare все локальные переменные исчезают
}
}
Важный момент: если локальная переменная — это ссылка на объект (например, String, Scanner, массив), то сама ссылка живёт в стеке, а вот объект — в куче! Когда ссылка исчезает, а других ссылок на объект нет, сборщик мусора может удалить объект.
Иллюстрация
Stack (для main):
| int a = 42 |
| args |
-------------------
Heap:
| [объекты, созданные через new] |
2. Утечки памяти в Java: миф или реальность?
Многие новички думают: «В Java же есть сборщик мусора! Значит, утечек памяти быть не может!» Увы, это миф, который развеивается на первом же большом проекте.
Сборщик мусора удаляет только те объекты, на которые нет ни одной живой ссылки. Если где-то осталась ссылка (пусть даже в самом неожиданном месте), объект будет висеть в памяти до победного конца. Или до OutOfMemoryError.
Пример 1: Статическая коллекция-ловушка
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakDemo {
// Ох, эта статическая коллекция!
private static final List<String> BIG_LIST = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
BIG_LIST.add("Строка номер " + i);
}
System.out.println("Добавлено миллион строк");
// Даже если main завершится, BIG_LIST останется в памяти, пока живёт JVM
}
}
Что происходит?
- Статическая переменная BIG_LIST живёт столько, сколько работает класс (а это обычно до конца жизни JVM).
- Все строки, добавленные в список, не могут быть удалены сборщиком мусора — на них всегда есть ссылка через BIG_LIST.
- Если вы случайно забыли очищать такие коллекции — получите утечку памяти.
Пример 2: Неотписанные слушатели (listeners)
import java.util.ArrayList;
import java.util.List;
class EventSource {
private final List<Runnable> listeners = new ArrayList<>();
public void addListener(Runnable listener) {
listeners.add(listener);
}
// ... другие методы ...
}
public class ListenerLeakDemo {
public static void main(String[] args) {
EventSource source = new EventSource();
Runnable listener = () -> System.out.println("Событие!");
source.addListener(listener);
// Если забыть вызвать source.removeListener(listener), listener останется в памяти навсегда!
}
}
Проблема: если слушатель больше не нужен, но его не удалили из списка — он и все объекты, на которые ссылается, останутся в памяти.
Пример 3: Кэш, который никогда не сбрасывается
import java.util.HashMap;
import java.util.Map;
public class CacheLeakDemo {
private static final Map<String, byte[]> CACHE = new HashMap<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
// Каждый раз создаём массив 1 КБ
CACHE.put("key" + i, new byte[1024]);
}
System.out.println("Добавлено миллион элементов в кэш");
// Кэш растёт, память заканчивается, OutOfMemoryError!
}
}
Вывод: даже с GC можно легко получить утечку памяти, если не следить за жизненным циклом объектов!
3. Слабые ссылки (WeakReference) и их друзья
Иногда нам нужны кэши или коллекции, где объекты могут быть удалены сборщиком мусора, если на них больше никто не ссылается. Для этого придуманы слабые ссылки (WeakReference).
Обычные (strong) ссылки
String s = new String("hello"); // strong-ссылка
Объект s будет жить в памяти, пока есть хотя бы одна strong-ссылка.
Слабая ссылка (WeakReference)
import java.lang.ref.WeakReference;
public class WeakRefDemo {
public static void main(String[] args) {
String strong = new String("Привет, мир!");
WeakReference<String> weak = new WeakReference<>(strong);
System.out.println("До очистки: " + weak.get()); // есть ссылка
strong = null; // убираем strong-ссылку
System.gc(); // просим GC почистить память (не гарантировано!)
// Через некоторое время weak.get() может стать null
System.out.println("После GC: " + weak.get());
}
}
Как это работает?
- Пока есть хотя бы одна strong-ссылка на объект, GC не удалит его.
- Если остались только слабые ссылки, объект может быть удалён при следующей сборке мусора.
- Метод weak.get() возвращает объект, если он ещё жив, или null, если объект удалён.
Где применяются слабые ссылки?
Главное применение — кэши, где не критично, если объект будет удалён из памяти. Например, если вы кэшируете изображения, но не хотите, чтобы кэш занимал всю память.
Пример: WeakHashMap
WeakHashMap — это коллекция, где ключи хранятся через слабые ссылки. Если на ключ больше никто не ссылается, соответствующая пара удаляется из карты.
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) {
Map<Object, String> map = new WeakHashMap<>();
Object key = new Object();
map.put(key, "Значение");
System.out.println("До очистки: " + map);
key = null; // убираем strong-ссылку на ключ
System.gc(); // просим GC поработать
// Через некоторое время карта станет пустой!
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("После GC: " + map);
}
}
Внимание: WeakHashMap работает только для ключей — значения хранятся обычными strong-ссылками.
Soft, Weak, Phantom: вся семья ссылок
В Java есть четыре типа ссылок (сортировка по «силе»):
| Тип ссылки | Когда объект удаляется GC? | Где применяется? |
|---|---|---|
|
Только когда нет ни одной strong-ссылки | Обычные переменные, коллекции |
|
При нехватке памяти | Кэш, который желательно держать подольше |
|
При следующем проходе GC, если нет strong-ссылок | Кэши, WeakHashMap, слушатели |
|
После финализации, для отслеживания удаления объекта | Спец. задачи, очистка вне кучи |
- SoftReference — объект удаляется при нехватке памяти (хорошо подходит для кэшей изображений и др.).
- WeakReference — удаляется при первой же сборке мусора, если нет других ссылок.
- PhantomReference — самый «призрачный» тип, нужен для сложных сценариев, редко используется новичками.
4. Практика: пример утечки памяти и её исправление
Пример утечки: статический список
import java.util.ArrayList;
import java.util.List;
public class LeakExample {
private static final List<byte[]> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new byte[1024 * 1024]); // 1 МБ
if (i % 10 == 0) System.out.println("Добавлено " + i + " МБ");
}
}
}
Что произойдёт?
Программа быстро съест всю доступную память и упадёт с OutOfMemoryError, потому что статический список хранит ссылки на все созданные массивы.
Исправление: использовать слабые ссылки
Если не критично, чтобы все объекты были доступны всегда, можно хранить их через слабые ссылки:
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class LeakFixed {
private static final List<WeakReference<byte[]>> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new WeakReference<>(new byte[1024 * 1024]));
if (i % 10 == 0) System.out.println("Добавлено " + i + " МБ");
System.gc(); // Подсказка GC (не гарантирует немедленной очистки!)
}
}
}
Теперь массивы могут быть удалены GC, если на них больше никто не ссылается — список хранит только слабые ссылки.
5. Типичные сценарии утечек памяти
- Слушатели событий: забыли удалить слушателя — объект живёт вечно.
- Статические коллекции: неочищаемые кэши, глобальные списки — всё это может привести к утечкам.
- Внутренние классы и лямбды: если внутренний класс или лямбда захватывает ссылку на внешний объект, он не будет удалён, пока живёт внешний объект.
Пример с внутренним классом
public class Outer {
private byte[] bigArray = new byte[1024 * 1024 * 100]; // 100 МБ
public Runnable createTask() {
// Анонимный внутренний класс захватывает ссылку на Outer!
return new Runnable() {
@Override
public void run() {
System.out.println("Задача выполняется");
}
};
}
public static void main(String[] args) {
Outer outer = new Outer();
Runnable task = outer.createTask();
// Даже если outer = null, task всё равно держит ссылку на bigArray!
}
}
Решение: используйте статические внутренние классы или выносите логику в отдельные классы, чтобы не держать лишние ссылки.
6. Практика: использование слабых ссылок в кэше
Добавим в учебное приложение простейший кэш с использованием WeakHashMap.
import java.util.Map;
import java.util.WeakHashMap;
public class ImageCache {
private final Map<String, byte[]> cache = new WeakHashMap<>();
public void put(String name, byte[] data) {
cache.put(name, data);
}
public byte[] get(String name) {
return cache.get(name);
}
public static void main(String[] args) {
ImageCache cache = new ImageCache();
cache.put("cat", new byte[1024 * 1024]); // 1 МБ
System.out.println("Котик добавлен в кэш");
// Если на ключ "cat" больше нет ссылок, объект может быть удалён GC
}
}
В реальных приложениях (например, в библиотеках изображений) слабые ссылки помогают избежать переполнения памяти за счёт автоматического удаления редко используемых данных.
7. Strong vs Weak: когда что использовать?
- Strong-ссылки — по умолчанию: используйте их для всего, что должно жить гарантированно.
- Слабые ссылки — для кэшей, слушателей, когда не критично, если объект будет удалён.
- Soft-ссылки — для кэшей, которые желательно держать подольше, но можно удалить при нехватке памяти.
- Phantom-ссылки — для продвинутых сценариев (например, финализация вне heap).
8. Типичные ошибки при работе с памятью и ссылками
Ошибка №1: «GC всё за меня уберёт!» Сборщик мусора удаляет только те объекты, на которые нет ни одной живой strong-ссылки. Если вы где-то «забыли» ссылку (например, в static-коллекции), объект будет жить вечно.
Ошибка №2: Забытые слушатели. Добавили слушателя к объекту, но не удалили его при уничтожении объекта? Слушатель и всё, что он захватил, останется в памяти.
Ошибка №3: Кэш без слабых ссылок. Используете обычный HashMap для кэша, который должен автоматически очищаться? Лучше используйте WeakHashMap или SoftReference.
Ошибка №4: Внутренние классы и лямбды захватывают внешний объект. Внутренние классы (и лямбда-выражения) неявно хранят ссылку на внешний объект. Если вы храните экземпляры таких классов дольше, чем сам внешний объект, получите утечку.
Ошибка №5: Ожидание немедленного срабатывания GC. Вызов System.gc() не гарантирует, что сборка мусора произойдёт сразу. Это лишь «просьба» к JVM, а не приказ.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ