Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Память и сборка мусора

В Java не нужно освобождать память вручную, как в C: ненужные объекты удаляет сборщик мусора (garbage collector, GC). Но понимать, где живут данные и когда объект становится «мусором», всё равно важно — иначе программа может съесть всю память или упасть с ошибкой.

Стек и куча

JVM хранит данные в двух местах. Стек (stack) — память каждого потока для вызовов методов: в нём лежат локальные переменные и параметры, и он очищается сам, когда метод заканчивается. Куча (heap) — общая память для всех объектов, созданных через new. Переменная примитивного типа хранит само значение, а переменная объектного типа — только ссылку на объект в куче:

void main() {
    int a = 5;
    int b = a;
    b = 10;
    IO.println("a = " + a);
 
    StringBuilder first = new StringBuilder("Анна");
    StringBuilder second = first;
    second.append(" и Олег");
    IO.println("first = " + first);
}

Результат:

a = 5
first = Анна и Олег

b = a скопировало число, а second = first — только ссылку: обе переменные указывают на один и тот же объект в куче, поэтому изменение через second видно и через first.

Сборка мусора

Объект нужен, пока до него можно добраться по ссылкам — из локальных переменных, статических полей, других живых объектов. Когда ссылок не осталось, объект становится недостижимым, и сборщик мусора освобождает его память. Здесь программа создаёт 10 ГБ массивов, хотя JVM разрешено всего 256 МБ (ключ -Xmx), а -Xlog:gc печатает каждую сборку:

void main() {
    long total = 0;
    for (int i = 1; i <= 100_000; i++) {
        byte[] report = new byte[100_000];
        total += report.length;
    }
    IO.println("Создано: " + total / 1_000_000 + " МБ");
}

Запуск:

java -Xmx256m -Xlog:gc Main.java

Результат (сокращён; время и числа у вас могут отличаться):

[0.018s][info][gc] Using G1
[1.687s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 48M->4M(256M) 6.739ms
[1.744s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 115M->4M(256M) 3.839ms
...
[4.158s][info][gc] GC(65) Pause Young (Normal) (G1 Evacuation Pause) 156M->3M(256M) 0.767ms
Создано: 10000 МБ

После каждой итерации массив report становится мусором. Запись 156M->3M значит, что до сборки куча была заполнена на 156 МБ, а после — всего на 3 МБ. Сборка заняла меньше миллисекунды.

Утечки памяти

В Java тоже бывают утечки памяти — когда ненужные объекты всё ещё достижимы, и сборщик не имеет права их удалить. Типичные причины:

  • статические коллекции: static List или static Map, куда добавляют «на всякий случай» и никогда не удаляют — статическое поле живёт всё время работы программы;
  • слушатели (listeners): объект подписался на события другого объекта, а отписаться забыл — источник событий держит ссылку на подписчика вечно;
  • кэши без ограничения размера и незакрытые ресурсы — файлы, соединения с базой.

Лечение одно: удалять ссылки, когда объект больше не нужен, — убирать элементы из коллекций, отписывать слушателей, ограничивать размер кэша.

StackOverflowError

Стек каждого потока невелик — обычно около мегабайта. Каждый вызов метода занимает в нём место, поэтому бесконечная рекурсия быстро его исчерпывает:

int depth = 0;
 
void dive() {
    depth++;
    dive();
}
 
void main() {
    try {
        dive();
    } catch (StackOverflowError e) {
        IO.println("Поймали: " + e);
        IO.println("Глубина: " + depth);
    }
}

Результат (глубина меняется от запуска к запуску):

Поймали: java.lang.StackOverflowError
Глубина: 19288

StackOverflowError и OutOfMemoryError — подклассы Error, а не Exception: это сбои самой JVM, и в настоящих программах их не перехватывают, а исправляют причину.

Какие бывают сборщики

В JDK несколько сборщиков мусора. По умолчанию работает G1 — он подходит большинству программ. ZGC (ключ -XX:+UseZGC) делает паузы короче миллисекунды даже на огромной куче, его выбирают для серверов, где важна быстрая реакция. Для маленьких программ есть простой Serial. Чаще всего из настроек памяти меняют только размер кучи: -Xmx — максимум, -Xms — начальный размер.

Частая ошибка

Статический кэш, в который только добавляют, растёт, пока память не кончится. Каждый отчёт сохранили «на всякий случай» — и ни один не может быть удалён:

static Map<Integer, byte[]> reportCache = new HashMap<>();
 
byte[] buildReport(int id) {
    byte[] report = new byte[10_000_000];
    reportCache.put(id, report);
    return report;
}
 
void main() {
    for (int id = 1; id <= 1000; id++) {
        buildReport(id);
    }
    IO.println("Все отчёты построены");
}

Запуск:

java -Xmx256m Main.java

Результат:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at Main.buildReport(Main.java:4)
	at Main.main(Main.java:11)

Java heap space означает, что в куче не осталось места даже после сборки мусора. Увеличивать -Xmx бесполезно — кэш съест и новую память. Нужно удалять старые записи или ограничить размер кэша, например хранить только последние сто отчётов.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.