Привет! В прошлой лекции мы впервые познакомились со встроенным механизмом языка Java — сборщиком мусора.
Он функционирует в фоновом режиме во время работы твоей программы, собирает ставшие ненужными объекты, которые в дальнейшем будут удалены. Таким образом, он освобождает память для создания новых объектов в будущем. В этой лекции мы подробнее разберем принцип его работы. Например, как и в какой момент объект становится ненужным? И как об этом узнает сборщик мусора? На эти вопросы и ответим :) Лекция у нас скорее обзорная: этот материал не надо заучивать наизусть. Он призван скорее расширить твой кругозор касаемо работы памяти и сборщика мусора, поэтому будет достаточно прочесть его и вынести что-то новое для себя :) Поехали! Первое, о чем тебе нужно помнить — сборщик мусора работает параллельно с твоей программой. Он не является ее частью и функционирует отдельно: для описания этого мы в прошлой лекции приводили аналогию с роботом-пылесосом. На самом деле, так было не всегда. Раньше сборщик мусора был устроен таким образом, что работал в одном потоке с твоей программой. И по какому-то графику, раз в сколько-то минут, начинал проверку на наличие в программе ненужных объектов. Проблема заключалась в том, что на время этой проверки и сборки мусора программа зависала и не выполнялась. Представь, что ты сидишь в офисе за работой. Но тут приходит уборщица, которой нужно вымыть полы в помещении. Она выгоняет тебя из-за компьютера на 5 минут и ты ждешь, пока она не закончит уборку. В это время ты работать не можешь. Вот примерно так раньше работали сборщики мусора :) Позднее этот механизм изменили, и теперь сборщик мусора работает в фоновом режиме, не тормозя работу самой программы. Ты уже знаешь, что объект умирает, когда на него не остается ни одной ссылки. Но на самом деле сборщик мусора не считает ссылки на объекты. Во-первых, это может быть достаточно долго. Во-вторых, не очень эффективно. Ведь объекты могут ссылаться друг на друга!
На рисунке изображен пример, когда 3 объекта ссылаются друг на друга, но больше на них не ссылается никто. То есть для работы остальной части программы они не нужны. Если бы сборщик мусора занимался просто подсчетом ссылок, все эти 3 объекта остались и не освободили бы память: ссылки-то на них есть! Это можно сравнить с космическим кораблем. Космонавты во время полета решили проверить список запчастей для ремонта и обнаружили среди них руль и педали от обычного автомобиля. Они явно тут не нужны и занимают лишнее место. Хоть эти детали связаны и имеют какие-то функции, в рамках работы космического корабля это ненужный мусор, от которого лучше избавиться. Поэтому в Java приняли решение сделать основой для сборки мусора не подсчет ссылок, а разделение объектов на два вида — достижимые и недостижимые. Как определить, является ли объект достижимым? Все гениальное просто. Объект считается достижимым, если на него ссылается другой достижимый объект. Получается такая “цепочка достижимости”. Она начинается при запуске программы и тянется в течение всего времени ее работы. Это выглядит примерно вот так:
Стрелка на рисунке обозначает выполняемый код нашей программы. В коде, например, в методе main(), создаются ссылки на объекты. Эти объекты могут ссылаться на новые объекты, те — еще на какие-то и так далее. Образуется цепочка ссылок объектов. Если от объекта по этой цепочке ссылок можно прийти к “корневой ссылке”, то есть той, которая непосредственно создается в выполняемом коде, — он считается достижимым. У нас на картинке они обозначены синим цветом. А вот если объект выпал из этой цепочки, то есть ни одна из переменных в коде, выполняемом в данный момент, не содержит ссылок на него, и по “цепочке ссылок” до него тоже невозможно добраться — он считается недостижимым. В нашей программе два таких объекта обозначены красным цветом. Обрати внимание: у этих “красных” объектов есть ссылки друг на друга. Но, как мы и сказали ранее, современный сборщик мусора в Java не занимается подсчетом ссылок. Он определяет достижимость либо недостижимость объекта. Поэтому два красных объекта на рисунке станут его добычей. Теперь рассмотрим весь процесс от начала до конца, а заодно посмотрим, как устроена память в Java :) Все объекты в Java хранятся в специальной области памяти, которая называется куча (heap). В обычном языке “кучей” называют гору предметов, где все подряд валяется вперемешку. Но куча в Java не такая. Она имеет очень логичную и разумную структуру. В один прекрасный день программисты Java обнаружили, что все объекты в их программах можно разделить на два типа — условно говоря, простые объекты и “долгожители”. “Долгожителями” считаются объекты, пережившие много сборок мусора. Чаще всего они будут существовать до конца работы программы. В итоге общая куча, где хранятся все созданные объекты, была разделена на несколько частей. Первая часть имеет красивое название — Eden (библ. “Райский сад”). Это отличное название, ведь именно сюда объекты попадают после их создания. Именно в этой части выделяется память для новых объектов, когда мы пишем new. Объектов может создаться много, и когда в этой области заканчивается место, начинается первая, “быстрая” сборка мусора. Надо сказать, что сборщик мусора очень умен и выбирает алгоритм работы в зависимости от того, чего в куче больше — мусора или рабочих объектов. Если почти все объекты являются мусором, сборщик помечает “живые” объекты и переносит их в другую область памяти, после чего текущая область очищается полностью. Если же мусора мало и большую часть занимают живые объекты, он помечает мусор, очищает его, а остальные объекты компонует. Мы сказали “сборщик помечает “живые” объекты и переносит их в другую область памяти”, но в какую? Область памяти, куда переносятся все объекты, пережившие хотя бы одну сборку мусора, называется Survival Space (“место для выживших”). Survival Space в свою очередь делится на поколения. Каждый объект относится к своему поколению в зависимости от того, сколько сборок мусора он пережил. Если одну — он относится к “Поколению 1”, если 5 — к “Поколению 5”. Вместе Eden и Survival Space образуют область под названием Young Generation (“молодое поколение”). Помимо Young Generation в куче существует и другая область памяти — Old Generation (“старое поколение”). Сюда как раз попадают те самые объекты-долгожители, которые пережили много сборок мусора. Их выгоднее хранить отдельно от всех остальных. И только когда область Old Generation заполнена, т.е. даже объектов-долгожителей в программе так много, что памяти не хватает, производится полная уборка мусора. Она обрабатывает не одну область памяти, а вообще все созданные Java-машиной объекты. Естественно, она занимает куда больше времени и ресурсов. Именно поэтому объекты-долгожители было решено хранить отдельно. Когда место заканчивается в других областях, проводится так называемая “быстрая сборка мусора”. Она охватывает только одну область, и за счет этого является более экономичной и быстрой. В конце, когда забита уже даже область для долгожителей, в бой вступает полная уборка. Таким образом, самый “тяжеловесный” инструмент используется сборщиком только тогда, когда без этого уже не обойтись. Схематично структура кучи и уборки выглядят так: