1. Введение
В этой статье мы рассмотрим некоторые вопросы об управлении памятью, которые часто возникают во время собеседований с разработчиками Java. Управление памятью — это область, с которой знакомы не так много разработчиков.
На самом деле разработчикам обычно не приходится иметь дело с этой концепцией напрямую, поскольку JVM позаботится о мельчайших деталях. Если что-то пойдет не так, даже опытные разработчики могут не располагать точной информацией об управлении памятью.
С другой стороны, эти концепции на самом деле довольно распространены в интервью, так что давайте сразу к делу.
2. Вопросы
Q1. Что означает утверждение «Память управляется в Java»?
Память является ключевым ресурсом, который требуется приложению для эффективной работы, и, как и любой другой ресурс, ее не хватает. Таким образом, его выделение и освобождение от приложений или различных частей приложения требует большой осторожности и внимания.
Однако в Java разработчику не нужно явно выделять и освобождать память — JVM и, в частности, сборщик мусора — должны обрабатывать выделение памяти, чтобы разработчику не приходилось этого делать.
Это противоречит тому, что происходит в таких языках, как C, где программист имеет прямой доступ к памяти и буквально ссылается на ячейки памяти в своем коде, создавая много места для утечек памяти.
Q2. Что такое сбор мусора и в чем его преимущества?
Сборка мусора — это процесс просмотра динамической памяти, определения того, какие объекты используются, а какие нет, и удаления неиспользуемых объектов.
Используемый объект или объект, на который ссылаются, означает, что некоторая часть вашей программы все еще поддерживает указатель на этот объект. На неиспользуемый объект или объект, на который нет ссылки, больше не ссылается ни одна часть вашей программы. Таким образом, память, используемая объектом, на который нет ссылки, может быть восстановлена.
Самым большим преимуществом сборки мусора является то, что он снимает с нас бремя ручного выделения/освобождения памяти, чтобы мы могли сосредоточиться на решении текущей проблемы.
Q3. Есть ли недостатки у сборки мусора?
Да. Всякий раз, когда запускается сборщик мусора, он влияет на производительность приложения. Это связано с тем, что все остальные потоки в приложении должны быть остановлены, чтобы поток сборщика мусора мог эффективно выполнять свою работу.
В зависимости от требований приложения это может стать реальной проблемой, неприемлемой для клиента. Однако эту проблему можно значительно уменьшить или даже устранить за счет умелой оптимизации и настройки сборщика мусора, а также использования различных алгоритмов GC.
Q4. Что означает термин «Остановить мир»?
Когда работает поток сборщика мусора, другие потоки останавливаются, то есть приложение на мгновение останавливается. Это аналогично уборке дома или фумигации, когда жильцам отказывают в доступе до тех пор, пока процесс не будет завершен.
В зависимости от потребностей приложения сборка мусора «остановить мир» может привести к неприемлемому зависанию. Вот почему важно настроить сборщик мусора и оптимизировать JVM, чтобы возникающее зависание было, по крайней мере, приемлемым.
Q5. Что такое стек и куча? Что хранится в каждой из этих структур памяти и как они взаимосвязаны?
Стек — это часть памяти, которая содержит информацию о вызовах вложенных методов вплоть до текущей позиции в программе. Он также содержит все локальные переменные и ссылки на объекты в куче, определенные в текущих выполняемых методах.
Эта структура позволяет среде выполнения возвращаться из метода, зная адрес, откуда он был вызван, а также очищать все локальные переменные после выхода из метода. Каждый поток имеет свой собственный стек.
Куча — это большой объем памяти, предназначенный для размещения объектов. Когда вы создаете объект с новым
ключевым словом, он размещается в куче. Однако ссылка на этот объект находится в стеке.
Q6. Что такое генерационная сборка мусора и что делает ее популярным подходом к сборке мусора?
Генерационный сбор мусора можно приблизительно определить как стратегию, используемую сборщиком мусора, при которой куча делится на несколько секций, называемых поколениями, каждая из которых будет содержать объекты в соответствии с их «возрастом» в куче.
Всякий раз, когда сборщик мусора работает, первый шаг в этом процессе называется маркировкой. Здесь сборщик мусора определяет, какие части памяти используются, а какие нет. Это может занять очень много времени, если необходимо проверить все объекты в системе.
По мере того, как выделяется все больше и больше объектов, список объектов растет и растет, что приводит к все большему и большему времени сборки мусора. Однако эмпирический анализ приложений показал, что большинство объектов недолговечны.
При сборке мусора по поколениям объекты группируются в соответствии с их «возрастом» с точки зрения того, сколько циклов сборки мусора они пережили. Таким образом, основная часть работы распределялась по различным второстепенным и крупным циклам сбора.
Сегодня почти все сборщики мусора являются генерационными. Эта стратегия так популярна, потому что со временем она оказалась оптимальным решением.
Q7. Подробно опишите, как работает сборка мусора на основе поколений
Чтобы правильно понять, как работает сборка мусора поколений, важно сначала вспомнить, как устроена куча Java, чтобы упростить сборку мусора поколений.
Куча делится на более мелкие пространства или поколения. Это «Молодое поколение», «Старое или постоянное поколение» и «Постоянное поколение».
Молодому поколению принадлежит большинство вновь созданных объектов . Эмпирическое исследование большинства приложений показывает, что большинство объектов недолговечны и, следовательно, вскоре становятся пригодными для сбора. Поэтому новые объекты начинают свое путешествие здесь и «выдвигаются» в пространство старого поколения только после достижения ими определенного «возраста».
Термин «возраст» в поколенческой сборке мусора относится к количеству циклов сборки, которые пережил объект .
Пространство молодого поколения далее разделено на три пространства: пространство Эдема и два пространства выживших, таких как Выживший 1 (s1) и Выживший 2 (s2).
В старом поколении размещаются объекты, которые живут в памяти дольше определенного «возраста» . В это пространство продвигаются объекты, пережившие сбор мусора от молодого поколения. Как правило, оно больше, чем у молодого поколения. Поскольку он больше по размеру, сбор мусора обходится дороже и происходит реже, чем в молодом поколении.
Постоянное поколение , или, как его чаще называют, PermGen,
содержит метаданные, необходимые JVM для описания классов и методов, используемых в приложении. Он также содержит пул строк для хранения интернированных строк. Он заполняется JVM во время выполнения на основе классов, используемых приложением. Кроме того, здесь могут храниться классы и методы библиотеки платформы.
Во- первых, любые новые объекты размещаются в пространстве Эдема . Обе ячейки выживших начинаются пустыми. Когда пространство Эдема заполняется, запускается небольшая сборка мусора. Ссылочные объекты перемещаются в первое оставшееся пространство. Объекты без ссылок удаляются.
Во время следующего малого GC то же самое происходит с пространством Эдема. Объекты, на которые нет ссылок, удаляются, а объекты, на которые есть ссылки, перемещаются в оставшееся пространство. Однако в этом случае они перемещаются во вторую ячейку выживших (S2).
Кроме того, возраст объектов из последнего второстепенного GC в первом оставшемся пространстве (S1) увеличивается, и они перемещаются в S2. Как только все выжившие объекты будут перемещены в S2, и S1, и пространство Эдема будут очищены. На данный момент S2 содержит объекты разного возраста.
На следующем минорном GC тот же процесс повторяется. Однако на этот раз места для выживших меняются местами. Ссылочные объекты перемещаются в 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
пространство кучи. ``
Q11. Можно ли «воскресить» объект, ставший подходящим для сборки мусора?
Когда объект становится пригодным для сборки мусора, 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 обрабатывает циклические ссылки. Он считает объекты живыми не тогда, когда на них есть какая-либо ссылка, а когда они достижимы при навигации по графу объектов, начиная с некоторого корня сборки мусора (локальная переменная живого потока или статическое поле). Если пара объектов с циклической ссылкой недоступна ни из одного корня, считается, что она подходит для сборки мусора.
Q14. Как строки представлены в памяти?
Экземпляр 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, поскольку интервьюер ожидает, что вы создали нетривиальные приложения, которые часто страдают от проблем с памятью.
Это не следует рассматривать как исчерпывающий список вопросов, а скорее как стартовую площадку для дальнейших исследований. Мы в ForEach желаем вам успехов в любых предстоящих интервью.