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, масив), то саме посилання живе в стеку, а от об’єкт — у купі! Коли посилання зникає, а інших посилань на об’єкт немає, збирач сміття може видалити об’єкт.
Ілюстрація
Стек (для main):
| int a = 42 |
| args |
-------------------
Купа:
| [об’єкти, створені через 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‑посилання — для просунутих сценаріїв (наприклад, фіналізація поза купою).
8. Типові помилки під час роботи з пам’яттю і посиланнями
Помилка № 1: «GC усе прибере за мене!» Збирач сміття видаляє лише ті об’єкти, на які немає жодного живого strong‑посилання. Якщо ви десь «забули» посилання (наприклад, у static‑колекції), об’єкт житиме вічно.
Помилка № 2: Забуті слухачі. Додали слухача до об’єкта, але не видалили його під час знищення об’єкта? Слухач і все, що він захопив, залишиться в пам’яті.
Помилка № 3: Кеш без слабких посилань. Використовуєте звичайний HashMap для кешу, який має автоматично очищатися? Краще використовуйте WeakHashMap або SoftReference.
Помилка № 4: Внутрішні класи та лямбди захоплюють зовнішній об’єкт. Внутрішні класи (і лямбда‑вирази) неявно зберігають посилання на зовнішній об’єкт. Якщо ви зберігаєте екземпляри таких класів довше, ніж сам зовнішній об’єкт, отримаєте витік.
Помилка № 5: Очікування негайного спрацювання GC. Виклик System.gc() не гарантує, що збирання сміття відбудеться одразу. Це лише «прохання» до JVM, а не наказ.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ