Java Garbage Collection

В предыдущей статье мы обсуждали метод finalize() у класса Object, который вызывался при сборке мусора и рассмотрели 2 случая, в ходе которых было не так понятно как работает Garbage Collection в Java. Поэтому давайте прочитаем оффициальную документацию от Оракла.

Garbage Collection basics

Итак, автоматическая сборка мусора в Java это процесс выявляения в памяти более неиспользуемых объектов (ссылок на них) и удаление. Это если говорить просто и в 2 словах. Теперь, давайте чуть детальней рассмотрим структуру памяти в JVM.

Когда мы запускаем наше приложение, то в первую очередь из Java классов формируются файлы с тем же именем, но с расширением .class, в которых находится обфусцированный джавабайткод. Происходит это потому, что с чистым джава кодом работает программист, а для машины он непонятен и приходится компилировать в понятный машине язык (Just-In-Time compiler). Во время исполнения (runtime) примитивы кладутся в память, называемую Thread stack, а объекты кладутся в Heap память. Именно с ней и работает сборщик мусора, что явно видно на рисунке.

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

Весь этот процесс происходит в несколько шагов. Рассмотрим детальней.

Шаг 1. Сначала сборщик мусора должен пометить объекты как неиспользуемые.

На первой диаграмме видно, что вся память забита объектами и они все используются. На второй полосе момент после того как сборщик мусора пометил объекты.

Шаг 2. Удаление неиспользуемых объектов и ссылок на них.

На картинке наблюдаем обычное удаление неиспользуемых объектов, по окончанию этого процесса в памяти остаются те объекты, ссылки на которых используются в программе. Как видим между ними остаются пустоты. Поэтому после обычного удаления идет процесс «утромбования».

Но как мы все понимаем, если каждый раз вызывать этот процесс и помечать объекты как неиспользуемые, после этого удалять и перемещать в начало, то сам процесс работы сборщика мусора довольно таки затратен. Поэтому, на самом деле, не все так просто как  показано на картинках. В действительности Heap память (куча) делится на несколько сегментов : Young Generation, Old Generation & Permanent Generation.

Young Generation это тот участок памяти, куда попадают новые объекты. Когда он заполняется, то происходит минорный сбор мусора. После которого объекты, которые не были собраны перемещаются в Old Generation периодически. Событие сбора мусора на минорной стадии называется Остановка Вселенной (Stop the World Event), потому что в это время все в приложении останавливается и ждет пока произойдет сбор мусора.

В Old Generation лежат все объекты, которые долго живут в приложении. Когда происходит сбор мусора в Old Generation это событие называют мажорный сбор мусора. Также как и минорный сбор мусора, мажорный также называется Остановка Вселенной. Поэтому важно для оптимизации приложения как можно реже вызывать сбор мусора.

Permanent Generation содержит метаданные, которые требует JVM для описания классов и методов, которые используются в приложении. Также в этой области хранятся классы и методы библиотеки Java SE.

А теперь еще подробнее посмотрим на каждый процесс аллокации и делокации в Young Generation.

Сначала объекты попадают в Eden, области Survivor s0, s1 пусты. После того как Eden заполняется, происходит процесс помечания объектов как «живых/мертвых» т.е. используемых и более неиспользуемых (есть ссылка на этот объект или более нет).

После процесса помечания все живые объекты переходят в область Survivor s0, а мертвые объекты удаляются в процессе minor garbage collection.

При следующей минорной сборке мусора происходит то же самое, но на этот раз объекты переходят также в область Survivor s1, т.е. те, которые были в s0 и не были удалены сборщиком устаревают (инкремент возраста) и переходят в s1. (цифры 1 и 2 показывают возраст объектов, т.е. сколько процессов сбора мусора они пережили)

При последующей сборке мусора происходит то же самое, но! На этот раз происходит переключение памяти survivor. Т.е. те объекты, которые лежали в s1 переходят в s0, а Eden и s1 очищаются.

После некоторого возраста объекты перемещаются в Old Generation. На рисунке это возраст после 8 (т.е. 8 раз произошел сбор мусора и эти объекты не были убиты).

Этот процесс называется Promotion (на рисунке написано Tenured, что и есть Old Generation)

Подведем итог. Новые объекты создаются и кладутся в Eden, после сбора мусора, когда память в Eden заполняется, те объекты, на которые есть ссылки кладутся в память Survivor, сначала в s0, при следующей очистке с увеличением возраста идут в s1 после чего при последующем сборе увеличивается возраст и Eden вместе с s1 очищаются, а все объекты, которые остались живыми кладутся в s0. После некоторого повторения все живые объекты переходят в Old Generation. Когда забивается Old Generation, происходит мажорный сбор мусора и (как это ни странно) если объектов слишком много и место не освобождается, то мы имеем ошибку OutOfMemory. Поэтому важно следить за использованием памяти и не хранить объекты и ссылки на них, если в них нет необходимости.

p.s Объемы памяти могут быть регулируемы и в некоторых случаях это весьма улучшит работоспособность приложения. Но рекомендуется с умом использовать ссылки, чтобы не приводить к состоянию OutOfMemory и/или Leak Memory (когда некоторые объекты, которые вы не используете не удаляются сборщиком мусора и долго живут в приложении).

Рубрика: Программирование Java | Оставить комментарий

Java. Класс Object и его методы. Часть 6 — finalize

В первом посте мы рассматривали все методы класса Object. Методы wait, notify и notifyAll мы не будем рассматривать сейчас, так как они относятся к многопоточному программированию, так что вернемся к ним позже. Вкратце, эти методы нужны для управления объектами из разных потоков.

Итак, остается последний метод в классе Object и это метод finalize(), который отмечен мофидикатором protected и имеет пустое тело, которое может бросить исключение. Пустое тело явно указывает на необходимость переопределения в наследнике. Но зачастую этот метод крайне редко вызывают. Итак, зачем он ?

Прочитаем документацию.

Сборщик мусора вызывает этот метод когда на объект больше нет ссылок. В наследнике нужно переопределить, чтобы каким-нибудь добавочным методом освободить некие ресурсы, например поток чтения/записи. Если после вызова метода finalize() бросается непроверяемое исключение, то оно будет игнорировано! Также пишется в документации, что метод не будет вызван более одного раза.

Для начала давайте рассмотрим простой пример. Создаем простой класс с 1 полем и в методе finalize() пропишем вывод в консоль информацию по элементу.

Теперь давайте создадим миллион экземпляров и посмотрим сколько раз вызовется этот метод.

После запуска программы мы увидим в мониторе нечто подобное:

Несколько десятков тысяч объектов после индекса 360 000 начали подчищаться сборщиком мусора и несколько тысяч после индекса 720 000. Это означает лишь то, что сборщик мусора выполняет свою работу тогда, когда необходима память для новых объектов (т.е. свободная память забивается при создании порядка 350 000 объектов).

Именно поэтому полагаться на вызов метода finalize() не стоит, так как вы никогда не будете уверены когда JVM понадобится свободная память и когда сборщик мусора выполнит свою работу.

Давайте тогда создадим поток в конструкторе нового класса и попробуем его закрыть в методе finalize().

Как видим, из 900 случаев метод finalize() сработал всего 4 раза. Это значит что 896 потоков остались незакрытыми. Поэтому полагаться только на метод finalize() не рекомендуется. Он может служить как дополнительный метод, но не как основной и единственный для освобождения ресурсов. В сети советуют использовать вызов этого метода для долгих процессов, в ходе которых нельзя быть уверенным, что ресурсы освободились и еще раз попытаться закрыть их.

Теперь закроем поток в блоке finally и напишем дополнительное закрытие в методе finalize(). Также поставим счетчик и выведем в конце количество раз когда вызвался метод finalize();

Теперь вызовы метода finalize() участились.

p.s. число 900 было найдено чисто эмпирически, при меньших повторах результат не виден. Можете увеличить количество повторов и наблюдать за вызовами метода finalize().

p.p.s. Еще одна возможность метода finalize() получить доступ к недоступному полю.

====================================

result:

This is finalize

MyObject still alive!

=====================================

So you may make an unreachable instance reachable in finalize method.

Рубрика: Программирование Java | Оставить комментарий

Java. Класс Object и его методы. Часть 5 — toString

Еще один метод класса Object, который в базовой реализации не является чем бы то ни было полезным и который, следовательно, имеет место быть переопределенным, это toString(). Как видим, в базовой версии метода берется имя класса и добавляется хешкод в обертке метода класса Integer, что в итоге и не дает сам хешкод.

В случае если не переопределять метод toString() в классе, то на выходе увидим такое. (для логирования можно явно не вызывать метод toString(), при попытке отобразить объект как строку этот метод вызывается неявно).

Что не дает никакой информации об объекте. Поэтому мы всегда переопределяем метод в классах. Для этого есть множество методов и шаблонов, но вы можете написать свою реализацию. Я рекомендую любой шаблон от Guava или Apache.

Единственный недостаток, так это случай, когда класс имеет родительский и нужно отобразить также информацию по данным родительского класса. Тогда нужно вручную обновить метод toString().

Метод toString() работает только для объектов. Если есть потребность в строковом представлении примитива, для этого есть метод у самого класса String.valueOf(..)

Рубрика: Программирование Java | Оставить комментарий

Java. Класс Object и его методы. Часть 4 — clone

Продолжаем рассматривать методы класса Object. Один из методов, который, на мой взгляд, мало используется программистами — это clone(). Суть метода в том, что он возвращает объект такого же типа, в котором значения всех полей скопированы. То же самое можно сделать и через конструктор, принимающий объект такого же класса.

Естественно, если сравнить их через == то выдаст false, так как они указывают на разные ссылки. Но сравнение через equals выдаст true.

Вопрос заключается в следующем — зачем писать еще один конструктор, когда сам язык предположил такую возможность? Но, как видим, в классе Object метод clone имеет модификатор доступа protected и это значит, что он недоступен из внешнего класса. Также метод может бросить исключение.

Прочитав документацию (курсор на методе, Ctrl + Q) можно понять, как его использовать.

Нужно реализовать в классе интерфейс Cloneable, который не имеет методов внутри, но он предоставляет классу возможность использовать метод clone и не бросать исключений.

Итак, после имплементации метода, можно изменить мофидикатор доступа у метода clone(), также можно внутри обработать исключение, а не передавать в наружный класс. Итого наш класс примет вид.

Также можно было пометить метод аннотацией @Nullable, хотя мы знаем, что после имплементации интерфейса исключение не должно бросаться. Ради интереса можно убрать интерфейс и посмотреть что будет. Как и ожидалось, видим сообщение об ошибке.

А теперь рассмотрим случай, когда один класс наследуется от другого и мы хотим при клонировании копировать и поля родительского класса. Для этого придется писать в родительском то же самое. Зато можно будет в текущем удалить обертку.

Для deepClone существуют и иные методы (более эффективные и менее эффективные, но простые). Суть проста — записываем данные в поток и читаем их при клонировании.

Давайте посмотрим на метод с SerializationUtils.clone(Object o)

Все вроде как упростилось. В родительском реализовали 1 интерфейс и по сути все. Никаких методов больше не нужно. Но вопрос в скорости, потому что сериализовать объекты долго и «дорого». Но, если у вас действительно большая иерархия классов, то проще использовать этот метод.

Рубрика: Программирование Java | Оставить комментарий

Java. Класс Object и его методы. Часть 3 — equals

В предыдущем посте мы обсуждали метод hashCode, который используется в методе equals. Как видим в базовой реализации в нем простое сравнивание через оператор == который сравнивает hashCode 2 объектов. Именно поэтому без переопределения метода equals толку от него не особо много.

Итак, метод equals должен обладать 4 свойствами

  • Симметричность: Для двух ссылок, a и b, a.equals(b) тогда и только тогда, когда b.equals(a)
  • Рефлексивность: Для всех ненулевых ссылок, a.equals(a)
  • Транзитивность: Если a.equals(b) и b.equals(c), то тогда a.equals(c)
  • Совместимость с hashCode(): Два тождественно равных объекта должны иметь одно и то же значение hashCode()

Для удовлетворения этих свойств сама среда разработки предлагает шаблон, в котором соблюдаются эти 4 правила. Посмотрим на примере ранее созданного класса CustomObject с 3 полями.

Итак, видим простые проверки, сначала проверяем по методу родительского класса, далее если объект хранит null или классы не совпадают, то возвращаем false. После этих простых проверок идут проверки по каждому полю и если любое из них не совпадает по значению, то возвращает false.

Именно потому, что метод может принимать null, мы можем с легкостью сравнивать объект c null. Но не наоборот!

Мало кто знает, но есть еще класс Objects, в котором  есть метод deepEquals, который принимает на вход 2 объектa и исходя из типа (если сам объект является массивом примитивов) вызывает нужный метод для сравнивания в другом классе (Arrays). Вот все методы класса Objects. Он является утилитным классом, который вызывает методы класса Object с некими добавочными проверками.

Но! Если вы будете сравнивать 2 объекта, которые могут хранить null, то deepEquals выдаст вам true. Так что имейте ввиду.

Но это уже решается с помощью метода requireNonNull, в котором бросается эксепшн если аргумент является null.

Рубрика: Программирование Java | Оставить комментарий

Java. Класс Object и его методы. Часть 2 — hashCode

Итак, в предыдущем посте мы ознакомились со списком методов класса Object и первым методом getClass() (ссылка). Далее идет метод hashCode и опять он помечен как native, но доступен для переопределения. Что важно, оно требует переопределения в классах, но обо всем по порядку. Метод возвращает хеш объекта в виде int. Давайте посмотрим, что это за число.

Дело в том, что это число генерируется (по умолчанию) рандомным способом (Park-Miller RNG). Но, это число постоянно для текущего объекта, что видно на примере цикличного вызова хешкода, так как записывается в заголовок объекта. 
Но для разных объектов оно будет разным.

Итак, зачем нужен метод hashCode? Когда мы сравниваем 2 примитива или объекта с помощью оператора ==, тогда сравниваются их hashCode. И если с примитивами все понятно (2 == 2 в любом случае), то с объектами дело обстоит иначе. Из-за случайной природы генерирования хешкода для объекта мы сталкиваемся со случаем, когда для двух разных объектов сгенерировались одинаковые хешкоды.


Я специально поставил в цикл 100 раз по миллиарду, чтобы наверняка наткнуться на такой кейс. Как видим вероятность мала, но она есть 7 к 100 млрд. И для того, чтобы избежать этой неприятности, в любом созданном классе нужно переопределить метод hashCode. Благо среда разработки предлагает шаблон.

Итого имеем метод, который учитывает все поля класса и генерит на их основе универсальный хешкод. В этом можно удостовериться повторно запустив метод с циклом в 100 миллиардов проверок. (Будьте осторожны, процессор может сильно нагреться)
На вопрос, откуда взялось число 31 как константа дает ответ книга Joshua Bloch Effective Java
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically.

Грубо говоря, 31 хорошее простое число, традиционно используемое для таких целей. Если использовать это число, то количество коллизий если и может быть, то будет минимальным (как видим, в нашем случае среди ста миллиардов повторов не произошла ни одна коллизия).

Итак, разобрались, почему нужно переопределять hashCode, теперь поговорим о методе equals, так как он тесно связан с получением хешкода. И это в следующем посте.

Рубрика: Программирование Java | 1 комментарий

Java. Класс Object и его методы. Часть 1

Изучение любого языка программирования начинается с его базового класса. Так как Java является объектно-ориентированным языком, то это значит, что у любого класса есть класс родитель, которым в Java является класс Object. Мы можем с легкостью посмотреть исходный код этого класса просто перейдя в него из любого другого класса. (пишем Object o; зажимая Ctrl/Command кликаем на Object). Чтобы схлопнуть все методы и javadoc над ними есть комбинация Ctrl Shift -.

Первым доступным методом (public) является Class<?> getClass(). Как видим он помечен также ключевым словом native и у него нет тела. Это означает что реализация на другом языке (С++). И так как метод помечен final его нельзя изменить в наследнике. Метод возвращает объект класса Class. Через него можно обращаться к полям и методам класса через Reflection (рефлексия). Самым часто используемым методом является getClass().getSimpleName(), который возвращает строковое название класса (удобно для использования в логировании).

Рубрика: Программирование Java | Оставить комментарий