«1. Введение
В этой статье мы рассмотрим некоторые вопросы об управлении памятью, которые часто возникают во время интервью с разработчиками Java. Управление памятью — это область, с которой знакомы не так много разработчиков.
На самом деле разработчикам обычно не приходится иметь дело с этой концепцией напрямую, так как JVM заботится о мельчайших деталях. Если что-то пойдет не так, даже опытные разработчики могут не располагать точной информацией об управлении памятью.
С другой стороны, эти понятия довольно распространены в интервью, так что давайте сразу к делу.
2. Вопросы
Q1. Что означает утверждение «Память управляется в Java»?
Память является ключевым ресурсом, который требуется приложению для эффективной работы, и, как и любой другой ресурс, ее не хватает. Таким образом, его выделение и освобождение от приложений или различных частей приложения требует большой осторожности и внимания.
Однако в Java разработчику не нужно явно выделять и освобождать память — JVM и, в частности, сборщик мусора — должны управлять распределением памяти, чтобы разработчику не приходилось этого делать.
Это противоречит тому, что происходит в таких языках, как C, где программист имеет прямой доступ к памяти и буквально ссылается на ячейки памяти в своем коде, создавая много места для утечек памяти.
Q2. Что такое сбор мусора и в чем его преимущества?
Сборка мусора — это процесс просмотра памяти кучи, определения того, какие объекты используются, а какие нет, и удаления неиспользуемых объектов.
Используемый объект или объект, на который ссылаются, означает, что некоторая часть вашей программы все еще поддерживает указатель на этот объект. На неиспользуемый объект или объект, на который нет ссылки, больше не ссылается ни одна часть вашей программы. Таким образом, память, используемая объектом, на который нет ссылки, может быть восстановлена.
Самым большим преимуществом сборки мусора является то, что он снимает с нас бремя ручного выделения/освобождения памяти, чтобы мы могли сосредоточиться на решении текущей проблемы.
Q3. Есть ли недостатки у сборки мусора?
Да. Всякий раз, когда запускается сборщик мусора, он влияет на производительность приложения. Это связано с тем, что все остальные потоки в приложении должны быть остановлены, чтобы поток сборщика мусора мог эффективно выполнять свою работу.
В зависимости от требований приложения это может стать реальной проблемой, неприемлемой для клиента. Однако эту проблему можно значительно уменьшить или даже устранить за счет умелой оптимизации и настройки сборщика мусора, а также использования различных алгоритмов GC.
Q4. Что означает термин «Остановить мир»?
Когда работает поток сборщика мусора, другие потоки останавливаются, то есть приложение на мгновение останавливается. Это аналогично уборке дома или фумигации, когда жильцам отказывают в доступе до тех пор, пока процесс не будет завершен.
В зависимости от потребностей приложения сборка мусора «остановить мир» может привести к неприемлемому зависанию. Вот почему важно настроить сборщик мусора и оптимизировать JVM, чтобы возникающее зависание было, по крайней мере, приемлемым.
В5. Что такое стек и куча? Что хранится в каждой из этих структур памяти и как они взаимосвязаны?
Стек — это часть памяти, которая содержит информацию о вложенных вызовах методов вплоть до текущей позиции в программе. Он также содержит все локальные переменные и ссылки на объекты в куче, определенные в текущих выполняемых методах.
Эта структура позволяет среде выполнения вернуться из метода, зная адрес, откуда он был вызван, а также очистить все локальные переменные после выхода из метода. Каждый поток имеет свой собственный стек.
Куча — это большой объем памяти, предназначенный для размещения объектов. Когда вы создаете объект с новым ключевым словом, он размещается в куче. Однако ссылка на этот объект находится в стеке.
«Q6. Что такое генерационная сборка мусора и что делает ее популярным подходом к сборке мусора?
Генерационный сбор мусора можно приблизительно определить как стратегию, используемую сборщиком мусора, при которой куча делится на несколько секций, называемых поколениями, каждая из которых будет содержать объекты в соответствии с их «возрастом» в куче.
Всякий раз, когда работает сборщик мусора, первый шаг процесса называется маркировкой. Здесь сборщик мусора определяет, какие части памяти используются, а какие нет. Это может занять очень много времени, если необходимо проверить все объекты в системе.
По мере того, как выделяется все больше и больше объектов, список объектов растет и растет, что приводит к все большему и большему времени сборки мусора. Однако эмпирический анализ приложений показал, что большинство объектов недолговечны.
При сборке мусора по поколениям объекты группируются в соответствии с их «возрастом» с точки зрения того, сколько циклов сборки мусора они пережили. Таким образом, основная часть работы распределялась по различным второстепенным и крупным циклам сбора.
Сегодня почти все сборщики мусора являются генерационными. Эта стратегия так популярна, потому что со временем она оказалась оптимальным решением.
В7. Подробное описание того, как работает сборка мусора по поколениям
Чтобы правильно понять, как работает сборка мусора по поколениям, важно сначала вспомнить, как устроена куча Java, чтобы облегчить сборку мусора по поколениям.
Куча делится на более мелкие пространства или поколения. Это «Молодое поколение», «Старое или постоянное поколение» и «Постоянное поколение».
Молодое поколение принимает большинство вновь созданных объектов. Эмпирическое исследование большинства приложений показывает, что большинство объектов недолговечны и, следовательно, вскоре становятся пригодными для сбора. Поэтому новые объекты начинают свое путешествие здесь и «передвигаются» в пространство старого поколения только после достижения ими определенного «возраста».
Термин «возраст» в поколенческой сборке мусора относится к числу циклов сборки, которые пережил объект.
Пространство молодого поколения далее разделено на три пространства: пространство Эдема и два пространства выживших, таких как Выживший 1 (s1) и Выживший 2 (s2).
Старое поколение содержит объекты, которые живут в памяти дольше определенного «возраста». В это пространство продвигаются объекты, пережившие сбор мусора от молодого поколения. Как правило, оно больше, чем у молодого поколения. Поскольку он больше по размеру, сбор мусора обходится дороже и происходит реже, чем в молодом поколении.
Постоянное поколение, или, как его чаще называют, PermGen, содержит метаданные, необходимые JVM для описания классов и методов, используемых в приложении. Он также содержит пул строк для хранения интернированных строк. Он заполняется JVM во время выполнения на основе классов, используемых приложением. Кроме того, здесь могут храниться классы и методы библиотеки платформы.
Во-первых, любые новые объекты помещаются в пространство Эдема. Обе ячейки выживших начинаются пустыми. Когда пространство Эдема заполняется, запускается небольшая сборка мусора. Ссылочные объекты перемещаются в первое оставшееся пространство. Объекты без ссылок удаляются.
Во время следующего минора GC то же самое происходит с пространством Эдема. Объекты, на которые нет ссылок, удаляются, а объекты, на которые есть ссылки, перемещаются в оставшееся пространство. Однако в этом случае они перемещаются во вторую ячейку выживших (S2).
Кроме того, возраст объектов из последнего второстепенного GC в первом оставшемся пространстве (S1) увеличивается, и они перемещаются в S2. Как только все выжившие объекты будут перемещены в S2, и S1, и пространство Эдема будут очищены. На данный момент S2 содержит объекты разного возраста.
На следующем минорном сборщике мусора тот же процесс повторяется. Однако на этот раз места для выживших меняются местами. Ссылочные объекты перемещаются в S1 как из Eden, так и из S2. Уцелевшие объекты состарены. Иден и S2 очищены.
«После каждого незначительного цикла сборки мусора проверяется возраст каждого объекта. Те, кто достиг определенного произвольного возраста, например, 8 лет, переводятся из молодого поколения в старое или постоянное поколение. Для всех последующих второстепенных циклов сборки объектов объекты будут по-прежнему перемещаться в пространство старого поколения.
Это в значительной степени исчерпывает процесс сборки мусора в молодом поколении. В конце концов, на старом поколении будет выполнена основная сборка мусора, которая очистит и уплотнит это пространство. На каждый основной GC приходится несколько второстепенных GC.
Q8. Когда объект становится пригодным для сборки мусора? Опишите, как Gc собирает допустимый объект?
Объект получает право на сборку мусора или сборщик мусора, если он недоступен из каких-либо активных потоков или любых статических ссылок.
Самый простой случай, когда объект становится пригодным для сборки мусора, это если все его ссылки пусты. Циклические зависимости без какой-либо активной внешней ссылки также подходят для GC. Таким образом, если объект A ссылается на объект B, а объект B ссылается на объект A и у них нет другой активной ссылки, то оба объекта A и B будут иметь право на сборку мусора.
Еще один очевидный случай — когда родительский объект имеет значение null. Когда объект кухни внутренне ссылается на объект холодильника и объект раковины, а объект кухни имеет значение null, и холодильник, и раковина получают право на сборку мусора вместе с их родителем, кухней.
Q9. Как запустить сборку мусора из кода Java?
Вы, как Java-программист, не можете форсировать сборку мусора в Java; он сработает только в том случае, если JVM решит, что ему нужна сборка мусора на основе размера кучи Java.
Перед удалением объекта из памяти поток сборки мусора вызывает метод finalize() этого объекта и дает возможность выполнить любую требуемую очистку. Вы также можете вызвать этот метод объектного кода, однако нет гарантии, что при вызове этого метода произойдет сборка мусора.
Кроме того, существуют такие методы, как System.gc() и Runtime.gc(), которые используются для отправки запроса на сборку мусора в JVM, но не гарантируется, что сборка мусора произойдет.
Q10. Что происходит, когда не хватает места в куче для хранения новых объектов?
Если нет места в памяти для создания нового объекта в куче, виртуальная машина Java выдает OutOfMemoryError или, точнее, java.lang.OutOfMemoryError пространство кучи.
В11. Можно ли «воскресить» объект, который стал пригодным для сборки мусора?
Когда объект становится пригодным для сборки мусора, GC должен запустить для него метод finalize. Метод finalize гарантированно запускается только один раз, поэтому сборщик мусора помечает объект как завершенный и дает ему паузу до следующего цикла.
В методе finalize вы можете технически «воскресить» объект, например, присвоив его статическому полю. Объект снова станет живым и не подходящим для сборки мусора, поэтому сборщик мусора не соберет его в следующем цикле.
Однако объект будет помечен как завершенный, поэтому, когда он снова станет приемлемым, метод finalize вызываться не будет. По сути, вы можете провернуть этот трюк с «воскрешением» только один раз за время существования объекта. Имейте в виду, что этот уродливый прием следует использовать только в том случае, если вы действительно знаете, что делаете, — однако понимание этого приема дает некоторое представление о том, как работает сборщик мусора.
Q12. Описать сильные, слабые, мягкие и фантомные ссылки и их роль в сборке мусора.
Подобно тому, как в Java осуществляется управление памятью, инженеру может потребоваться выполнить как можно большую оптимизацию, чтобы свести к минимуму задержку и максимизировать пропускную способность в критически важных приложениях. Хотя невозможно явно контролировать запуск сборки мусора в JVM, можно повлиять на то, как это происходит в отношении созданных нами объектов.
«Java предоставляет нам ссылочные объекты для управления отношениями между создаваемыми нами объектами и сборщиком мусора.
По умолчанию на каждый объект, который мы создаем в программе Java, строго ссылается переменная:
StringBuilder sb = new StringBuilder();
В приведенном выше фрагменте ключевое слово new создает новый объект StringBuilder и сохраняет его в куче. Затем переменная sb сохраняет сильную ссылку на этот объект. Для сборщика мусора это означает, что конкретный объект StringBuilder вообще не подлежит сбору из-за строгой ссылки, удерживаемой на него sb. История меняется только тогда, когда мы обнуляем sb следующим образом:
sb = null;
После вызова приведенной выше строки объект будет иметь право на сбор.
Мы можем изменить эту связь между объектом и сборщиком мусора, явно заключив его в другой ссылочный объект, который находится внутри пакета java.lang.ref.
Мягкая ссылка на указанный выше объект может быть создана следующим образом:
StringBuilder sb = new StringBuilder();
SoftReference<StringBuilder> sbRef = new SoftReference<>(sb);
sb = null;
В приведенном выше фрагменте мы создали две ссылки на объект StringBuilder. Первая строка создает сильную ссылку sb, а вторая — мягкую ссылку sbRef. Третья строка должна сделать объект пригодным для сбора, но сборщик мусора отложит его сбор из-за sbRef.
История изменится только тогда, когда памяти станет не хватать, а JVM окажется на грани выдачи ошибки OutOfMemory. Другими словами, объекты только с программными ссылками собираются в качестве крайней меры для восстановления памяти.
Слабая ссылка может быть создана аналогичным образом с помощью класса WeakReference. Когда sb имеет значение null, а объект StringBuilder имеет только слабую ссылку, сборщик мусора JVM не будет иметь абсолютно никаких компромиссов и немедленно соберет объект в самом следующем цикле.
Фантомная ссылка похожа на слабую ссылку, и объект только с фантомными ссылками будет собран без ожидания. Однако фантомные ссылки помещаются в очередь сразу после сбора их объектов. Мы можем опросить очередь ссылок, чтобы точно узнать, когда объект был собран.
Q13. Предположим, у нас есть циклическая ссылка (два объекта, которые ссылаются друг на друга). Может ли такая пара объектов получить право на сборку мусора и почему?
Да, пара объектов с циклической ссылкой может стать подходящей для сборки мусора. Это связано с тем, как сборщик мусора Java обрабатывает циклические ссылки. Он считает объекты живыми не тогда, когда на них есть какая-либо ссылка, а когда они достижимы при навигации по графу объектов, начиная с некоторого корня сборки мусора (локальная переменная живого потока или статическое поле). Если пара объектов с циклической ссылкой недоступна ни из одного корня, считается, что она подходит для сборки мусора.
В14. Как строки представлены в памяти?
Экземпляр String в Java — это объект с двумя полями: полем значения char[] и хэш-полем int. Поле значения представляет собой массив символов, представляющих саму строку, а поле хэша содержит хэш-код строки, которая инициализируется нулем, вычисляется во время первого вызова hashCode() и с тех пор кэшируется. В качестве любопытного пограничного случая, если hashCode строки имеет нулевое значение, его необходимо пересчитывать каждый раз, когда вызывается hashCode().
Важно то, что экземпляр String неизменяем: вы не можете получить или изменить базовый массив char[]. Другая особенность строк заключается в том, что статические постоянные строки загружаются и кэшируются в пуле строк. Если у вас есть несколько идентичных объектов String в исходном коде, все они представлены одним экземпляром во время выполнения.
Q15. Что такое StringBuilder и каковы варианты его использования? В чем разница между добавлением строки в StringBuilder и объединением двух строк с помощью оператора +? Чем Stringbuilder отличается от Stringbuffer?
«StringBuilder позволяет манипулировать последовательностями символов, добавляя, удаляя и вставляя символы и строки. Это изменяемая структура данных, в отличие от неизменяемого класса String.
При объединении двух экземпляров String создается новый объект, а строки копируются. Это может привести к огромным накладным расходам сборщика мусора, если нам нужно создать или изменить строку в цикле. StringBuilder позволяет намного эффективнее обрабатывать манипуляции со строками.
StringBuffer отличается от StringBuilder тем, что он потокобезопасен. Если вам нужно манипулировать строкой в одном потоке, используйте вместо этого StringBuilder.
3. Заключение
В этой статье мы рассмотрели некоторые из наиболее распространенных вопросов, которые часто возникают на собеседованиях с Java-инженерами. Вопросы об управлении памятью в основном задают кандидатам на должность старшего разработчика Java, поскольку интервьюер ожидает, что вы создали нетривиальные приложения, которые часто страдают от проблем с памятью.
Это не исчерпывающий список вопросов, а скорее стартовая площадка для дальнейших исследований. Мы в Baeldung желаем вам успехов в любых предстоящих интервью.