Тема 30. Ссылки - BelyiZ/JavaCourses GitHub Wiki
Как мы уже узнали в предыдущих лекциях объекты в java хранятся в определенной области памяти под названием куча. У этой области есть определенный объем и в том случае, когда этот объем израсходован виртуальная машина бросает OutOfMemoryException
.
Иногда возникают такие ситуации в которых приходится оперировать большим количеством тяжелых объектов или в условиях ограниченной памяти. Давайте рассмотрим, что может предложить JDK
для таких случаев.
SoftReference<T>
- это класс обертка в который можно положить какой-либо объект. При этом JVM
будет работать с этим объектом несколько иначе чем с обычной сильной ссылкой.
String text = “…”;
// <- сильная ссылка
Референт – объект который лежит внутри мягкой ссылки будет доступен с помощью метода get()
до тех пор пока у кучи достаточно памяти для нормальной работы. Но если памяти начнет не хватать, то GC
гарантированно освободит место, занимаемое вложенным объектом прежде чем кинуть OutOfMemoryException
. Этот функционал используется для кэширования.
#Сильные ссылки
Это самые популярные ссылочные типы, к которым все привыкли. Объект в куче не удаляется сборщиком мусора, пока на него указывает сильная ссылка или если он явно доступен через цепочку сильных ссылок.
#Слабые и мягкие ссылки
- Мягкие ссылки полезны для кэшей, чувствительных к доступному объёму оперативной памяти.
- Объекты по ним могут зачиститься, но только в случае необходимости.
- Например, если нужно создать ещё объектов с сильными ссылками, а уже негде, лучше освободить кэш и замедлить работу, чем уронить процесс напрочь.
Если вкратце, сборщик мусора освободит память объекта, если на него указывают только слабые ссылки.
Когда на объект указывают мягкие ссылки SoftReferences
, то освобождение памяти происходит, когда JVM
сильно нуждается в памяти.
Это дает определенное преимущество SoftReference
перед Strong ссылкой в определенных случаях.
Например, SoftReference
используют для реализации кэша приложений, поэтому JVM
первым делом удалит объекты, на которые указывают только SoftReferences
.
Пример SoftReference
в Java:
// какой-то объект
Student student = new Student();
// слабая ссылка на него
SoftReference<Student> softStudent = new SoftReference<Student>(student)
// теперь объект Student может быть собран сборщиком мусора
// но это случится только в случае сильной необходимости JVM в памяти
student = null;
WeakReference
отлично подходит для хранения метаданных, например, для хранения ссылки на ClassLoader
.
Если ни один класс не загружен, то не стоит ссылаться на ClassLoader
.
Именно поэтому WeakReference делает возможность сборщику мусора выполнить свою работу с ClassLoader
, как только на него удалится последняя сильная ссылка.
Пример WeakReference
в Java:
// какой-то объект
Student student = new Student();
// слабая ссылка на него
WeakReference<Student> weakStudent = new WeakReference<Student>(student);
// теперь объект Student может быть собран сборщиком мусора
student = null;
Слабые ссылки полезны для сопоставления объектов чему-нибудь без удерживания их от зачистки когда они больше не нужны (например Map<Ключ, WeakRef<Значение>>). На возможность зачистки они не влияют, слабые ссылки будут очищены при очередном запуске сборщика.
Также существуют Фантомные ссылки.
Фантомные ссылки это один из способов финализировать объект. Помещая объект в фантомную ссылку и зануляя его сильные ссылки мы говорим GC
что его можно поставить в список на удаление. Однако, в отличие от слабых и мягких ссылок, GC
не освободит память занимаемую объектом пока ссылка не будет явно очищена с помощью метода clear()
или не будет доступна для "живых" объектов программы. В это время можно выполнить какие-либо действия необходимые перед полным удалением объекта, например, освободить ресурсы. Чтобы исключить использование референта фантомной ссылки, который помечен на удаление, метод get()
для этого типа ссылок всегда возвращаетnull
.
Все три типа ссылок являются наследниками абстактного класса Reference
из которого наследуют свои методы. Начиная с Java 9
в этот класс был добавлен статический метод reachabilityFence(Object ref)
который позволяет пометить переданный объект как всегда доступный по сильной ссылке, то есть защитить его от удаления.
Таким образом мы с вами рассмотрели некоторые ключевые инструменты общения с GC
доступные в 'Java'. Для того чтобы информация о типах ссылок в 'Java' лучше усвоилась давайте рассмотрим следующую аналогию: пусть куча — это некоторый бар, а GC
это охранник в этом баре. Сильные ссылки — это завсегдатаи этого бара, друзья бармена и охранника. Они могут оставаться внутри сколько захотят. Когда они окажутся так сказать вне досягаемости, GC, то есть охранник проводит их на выход. Soft references это друзья друзей. Они тоже могут находиться в баре сколько угодно, но если в баре не окажется места для новых посетителей, то их первыми попросят на выход. Week & Phantom references это представители маргинальных слоёв общества. Им в баре не рады и если они никак не связаны с основными посетителями, то их сразу вышвырнут на улицу. У Phantom references
при этом ещё можно и карманы обчистить.
#Утечки памяти
Утечка памяти — это ситуация, когда в куче присутствуют объекты, которые больше не используются, но сборщик мусора не может удалить их из памяти и, таким образом, они сохраняются там без необходимости.
Утечка памяти плоха тем, что она блокирует ресурсы памяти и со временем снижает производительность системы.
Если с ней не бороться, приложение в конечном итоге исчерпает свои ресурсы и завершится с фатальной ошибкой java.lang.OutOfMemoryError
.
Существует два различных типа объектов, которые находятся в Heap-памяти (куче) — со ссылками и без них. Объекты со ссылками — это те, на которые имеются активные ссылки внутри приложения, в то время как на другие нет таких ссылок.
Сборщик мусора периодически удаляет объекты без ссылок, но он никогда не собирает объекты, на которые все еще ссылаются. В таких случаях могут возникать утечки памяти:
Признаки утечки памяти
-
Серьезное снижение производительности при длительной непрерывной работе приложения
-
Ошибка кучи
OutOfMemoryError
в приложении -
Спонтанные и странные сбои приложения
-
В приложении время от времени заканчиваются объекты подключения
Типы утечек памяти в Java
В любом приложении утечка памяти может произойти по множеству причин. Рассмотрим наиболее частые причины.
- Утечка памяти через статические поля Первый сценарий, который может привести к потенциальной утечке памяти, — это интенсивное использование статических переменных.
В Java
статические поля имеют срок жизни, который обычно соответствует полному жизненному циклу запущенного приложения (за исключением случаев, когда ClassLoader
получает право на сборку мусора).
Приведем Java
-программу, которая заполняет статический список:
public class StaticTest {
public static List<Double> list = new ArrayList<>();
public void populateList() {
for (int i = 0; i < 10000000; i++) {
list.add(Math.random());
}
Log.info("Debug Point 2");
}
public static void main(String[] args) {
Log.info("Debug Point 1");
new StaticTest().populateList();
Log.info("Debug Point 3");
}
}
если отбросить ключевое слово static, то память очистится.
Если коллекции или большие объекты объявлены как статические, то они остаются в памяти на протяжении всего времени работы приложения, тем самым блокируя жизненно важную память, которую можно было бы использовать в другом месте.
-
Советы для работы:
-
Минимизируйте использование статических переменных
-
При использовании синглтонов полагайтесь на имплементацию, которая лениво, а не жадно загружает объект.
Утечка памяти через незакрытые ресурсы
Всякий раз, когда создается новое соединение или открываем поток, JVM
выделяет память для этих ресурсов.
В качестве примера можно привести соединения с базой данных, входные потоки и объекты сессий.
Забыв закрыть эти ресурсы, можно заблокировать память, что сделает их недоступными для GC. Это может произойти даже в случае исключения, которое не позволяет программному процессу достичь оператора, выполняющего код для закрытия этих ресурсов.
В любом случае, открытые соединения, оставшиеся от ресурсов, потребляют память, и если с ними не разобраться, они могут ухудшить производительность и даже привести к ошибке OutOfMemoryError
.
-
Советы для работы:
-
Всегда используйте блок
finally
для закрытия ресурсов -
Код (даже в блоке
finally
), закрывающий ресурсы, сам не должен содержать исключений. -
При использовании Java 7+ можно использовать блок
try-with-resources
.
Неверные реализации equals()
и hashCode()
При написании новых классов очень распространенной ошибкой является некорректное написание переопределяемых методов equals()
и hashCode()
.
HashSet
и HashMap
используют эти методы во многих операциях и если они не переопределены правильно, то эти методы могут стать источником потенциальных проблем, связанных с утечкой памяти.
Возьмем для примера простой класс Person
и используем его в качестве ключа для HashMap
:
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
}
Теперь вставим дубликаты объектов Person
в Map
, которая использует их в качестве ключа.
Отметим, что Map не может содержать дубликаты ключей:
@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
Map<Person, Integer> map = new HashMap<>();
for(int i = 0; i < 100; i++) {
map.put(new Person("jon"), 1);
}
Assert.assertFalse(map.size() == 1);
}
Поскольку Map
не позволяет использовать дубликаты ключей, многочисленные объекты Person
, которые мы добавили, не должны увеличить занимаемую ими пространство в памяти.
Поскольку мы не определили правильные метод equals()
, дублирующие объекты накопились и заняли память.
Однако, если бы мы правильно переопределили методы equals()
и hashCode()
, тогда в Map
существовал бы только один объект Person
.
Приведем правильные реализации equals()
и hashCode()
для класса Person
:
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Person)) {
return false;
}
Person person = (Person) o;
return person.name.equals(name);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
return result;
}
}
И в этом случае наш тест сработает корректно:
@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
Map<Person, Integer> map = new HashMap<>();
for(int i = 0; i < 2; i++) {
map.put(new Person("jon"), 1);
}
Assert.assertTrue(map.size() == 1);
}
Другим примером является использование ORM
, например Hibernate
, который использует методы equals()
и hashCode()
для анализа объектов и сохранения их в кеше.
Если эти методы не переопределены, то шансы утечки памяти довольно высоки, потому что Hibernate
не сможет сравнивать объекты и заполнит свой кеш их дубликатами.
-
Советы для работы:
-
Взять за правило, при создании новых сущностей (
Entity
), всегда переопределять методыequals()
иhashCode()
. -
Не достаточно просто переопределить эти методы. Они должны быть переопределены оптимальным образом.
Утечка памяти когда внутренние классы, ссылаются на внешние классы
Не статическим внутренним классам (анонимным) для инициализации всегда требуется экземпляр внешнего класса. Каждый нестатический внутренний класс по умолчанию имеет неявную (скрытую) ссылку на класс в котором он находится. Если используется этот объект внутреннего класса в приложении, то даже после того, как объект внешнего класса завершает свою работу, он не будет утилизирован сборщиком мусора.
Рассмотрим класс, содержащий ссылку на множество громоздких объектов и имеющий не статический внутренний класс.
Модель памяти будет перегружена если мы создаем объект только внутреннего класса
Если мы просто объявим внутренний класс как статический, то та же модель памяти не будет перегружена.
Это происходит потому, что объект внутреннего класса содержит скрытую ссылку на объект внешнего класса, тем самым делая его недоступным для сборщика мусора. То же самое происходит и в случае анонимных классов.
-
Советы для работы:
-
Если внутреннему классу не нужен доступ к членам внешнего класса, подумайте о превращении его в статический класс.
Утечка памяти finalize() методы
Использование финализаторов является еще одним потенциальным источником утечек памяти.
Когда в классе переопределяется метод finalize()
, объект этого класса не убирается сборщиком мусора немедленно.
Вместо этого он помещается сборщиком в очередь на утилизацию, которая происходит немного позже.
Кроме того, если код, написанный в методе finalize()
, переопределен неоптимально, и если очередь финализатора не может идти в ногу со сборщиком мусора Java
, то рано или поздно нашему приложению суждено встретить ошибку OutOfMemoryError
.
Пояснение: методы finalize()
вызываются последовательно в том порядке, в котором были добавлены в список сборщиком мусора.
Соответственно, если какой-то finalize()
зависнет, он подвесит поток "Finalizer", но не сборщик мусора.
Это в частности означает, что объекты, не имеющие метода finalize()
, будут исправно удаляться, а вот имеющие будут добавляться в очередь, пока не отвиснет поток "Finalizer", не завершится приложение или не кончится память.
- Советы для работы:
- Избегать финализаторов.
Утечка памяти при использовании интернированных строк
В Java 7 пул строк претерпел значительные изменения: он был перенесен из PermGen
в HeapSpace
(подробнее об этом можно прочитать в статье PermGen
и Metaspace
в среде Java
).
Но в приложениях, работающих на версии 6 и ниже, мы должны быть более внимательными при работе с большими строками.
Когда читается большой строковый объект и вызываем у него метод intern()
, то он сохраняется в пул строк, который находиться в PermGen
(постоянная память) и остается там до тех пор, пока приложение работает.
Это блокирует память и приводит к серьезным утечкам в приложении.
- Советы для работы:
- Самый простой способ решить эту проблему — обновиться до последней версии Java, поскольку пул строк был перемещен в пространство кучи, начиная с 7 версии.
- При работе с большими строками можно увеличить размер
PermGen
, что позволит избежать ошибкиOutOfMemoryError
:
* -XX:MaxPermSize=512m
Утечка памяти при использовании ThreadLocals
ThreadLocal — это механизм, который позволяет изолировать состояние (значения переменных) в определенном потоке, что делает его безопасным.
При использовании этой конструкции, каждый поток будет содержать неявную ссылку на его копию переменной ThreadLocal
и будет хранить свою собственную копию, вместо того чтобы совместно использовать ресурс через множество потоков, так долго, сколько поток будет жить.
Несмотря на свои преимущества, использование переменных ThreadLocal
противоречиво, поскольку они могут являться причиной утечек памяти, если они не используются должным образом.
Предполагается что ThreadLocal
переменные будут собраны сборщиком мусора после того, как содержащий их поток перестанет существовать.
Но существует проблема с использованием ThreadLocal
в современных серверах приложений.
Современные сервера приложений используют пул потоков для обработки запросов, вместо создания нового потока на каждый запрос. Кроме того, они используют отдельный загрузчик классов.
Поскольку пулы потоков в серверах приложений работают по принципу повторного использования потоков, они никогда не удаляются сборщиком мусора — вместо этого они повторно используются для обслуживания другого запроса.
Итак, если класс создает ThreadLocal
переменную, но не удаляет ее явно, то копия этого объекта останется в рабочем потоке даже после остановки веб-приложения, тем самым не позволяя сборщику удалить этот объект.
-
Советы для работы:
-
Хорошей практикой является очищение
ThreadLocal
переменных, когда они больше не используются.ThreadLocal
предоставляет методremove()
, который удаляет значение переменной для текущего потока -
Не использовать
ThreadLocal. set (null)
для очистки значения — на самом деле оно не очищает значение, а вместо этого ищет мапу, связанную с текущим потоком, и устанавливает пару ключ-значение — текущий поток и null соответственно -
Еще лучше рассмотреть
ThreadLocal
как ресурс, который необходимо закрыть в блокеfinally
, чтобы убедиться, что он всегда будет закрыт, даже в случае исключения:
try {
threadLocal.set(System.nanoTime());
//... further processing
} finally {
threadLocal.remove();
}
Другие стратегии для борьбы с утечками памяти
Несмотря на то, что при работе с утечками памяти нет единого решения для всех случаев, есть некоторые способы, с помощью которых возможно минимизировать эти утечки.
-
Включить профилирование
Java
-профайлеры — это инструменты, которые контролируют и диагностируют утечки памяти. Они анализируют, что происходит внутри приложения, например, как распределяется память. Используя профайлеры, возможно сравнивать различные подходы и находить области, где возможно оптимально использовать ресурсы. -
Детальная сборка мусора Включая режим детальной сборки мусора мы можем отслеживать подробности, происходящие при работе GC.
-
Чтобы включить этот функционал, нужно добавить следующую настройку
JVM
:
-verbose:gc
Включив этот параметр, возможно увидеть детали того, что происходит внутри сборщика мусора.
-
Использовать ссылочные объекты, чтобы избежать утечек памяти Возможно прибегнуть к ссылочным объектам в
Java
, которые встроены в пакетjava.lang.ref
для устранения утечек памяти. Используя пакетjava.lang.ref
, вместо прямых ссылки на объекты, используются специальные ссылки на объекты, которые способствуют легкому удалению сборщиком мусора этих объектов. -
Бенчмаркинг Возможно измерять и анализировать производительность
Java
-кода, выполняя тесты. Таким образом, возможно сравнивать эффективность альтернативных подходов для выполнения одной и той же задачи. Это может помочь выбрать лучший подход и сохранить память.
Тема 29. Структура памяти | Оглавление | Тема 31. Введение в многопоточность