Эта серия посвящена еще одному методу класса Object. Сразу хочу сказать, что в большинстве случаев от использования этого метода нужно отказываться. А вот почему это нужно делать и зачем он тогда вообще нужен? - об этом и пойдет речь далее...
protected void finalize() throws Throwable;
Для начала взглянем на документацию к методу:
/**
* Вызывается сборщиком мусора (GC) для объекта в тот момент, когда GC
* определил, что на объект больше нет ссылок. Переопределение метода
* finalize в подклассах предназначено для освобождения системных ресурсов
* и выполнения других необходимых очисток.
*
* Основной целью метода finalize является его вызов в момент когда Java
* виртуальной машиной установлено, что объект более недостижим из живых
* потоков, за исключением тех случаев, когда метод finalize вызывается
* повторно. В методе finalize могут выполняться любые действия, в том
* числе и те, в следствии которых объект снова станет доступным. Но как
* правило, до того как объект будет безвозвратно удален, выполняют
* необходимую очистку. Например, метод finalize объекта, который
* представляет из себя I/O соединение, может содержать вызов закрытия
* этого соединения до того как объект будет удален.
*
* Реализация метод finalize в классе Object не содержит никаких действий,
* она пустая. Но в подклассах можно переопределить эту реализацию.
*
* Спецификацией Java не гарантируется, что метод finalize будет когда
* либо вызван для кого либо объекта. Но гарантируется, что поток,
* вызывающий метод finalize, в момент вызова не будет блокировать какие
* либо данные объекта. Если в момент выполнения метода будет брошено
* исключение, то оно будет проигнорировано и метод завершится.
*
* После того как для объекта был выполнен метод finalize, никаких
* действий не будет выполнено, пока JVM снова не проверит объект на
* недоступность из живых потоков и только потом, по результатам проверки,
* объект может быть удален из памяти.
*
* Метод finalize не может быть вызван JVM более одного раза для каждого
* объекта.
*
* Любое исключение, выброшенное в методе finalize, завершит выполнение
* очистки, но будет проигнорировано.
*/
Почему же стоит избегать использования метода finalize?.. Да потому что об этом даже в javadoc написано :-). Метод finalize очень ненадежный и полагаться на него нельзя. Тогда для чего же он нужен?... Пожалуй, с этого и стоит начать.
Применить finalize можно для освобождения ресурсов, которые не связаны с памятью JVM. К таким ресурсам можно отнести ресурсы операционной системы, ресурсы базы данных и т.д. Если какой либо объект занимает внешние ресурсы, то конечно их необходимо освободить, после того как объект более недостижим. А поскольку, внешние ресурсы не связаны с памятью JVM, сборщик мусора не сможет до них дотянуться и освободить. В этих случаях, для освобождения внешних ресурсов, можно применить finalize. Но если освобождение этих ресурсов критично, то использовать finalize нельзя, так как он может вообще никогда не вызваться или вызваться когда будет уже поздно.
Еще один способ применения finalize - это своего рода защита от дурака. В этом случае, он может использоваться для подстраховки пользователя класса. Например, есть некий класс, который содержит в себе следующие методы:
- метод open для получения каких нибудь внешних ресурсов;
- метод close для явного освобождения полученных ресурсов.
Для страховки в методе finalize можно проверять были ли освобождены занимаемые ресурсы. Если окажется, что пользователь класса забыл их освободить, то можно сделать это за него. Конечно, при таком подходе нельзя со стопроцентной уверенностью сказать, что класс будет защищен от всех ошибок пользователя, но, по крайней мере, он будет более живучим.
При переопределении метода finalize, необходимо вызвать реализацию суперкласса. Если этого не сделать, то finalize суперкласса никогда не будет вызван.
protected void finalize() throws Throwable {
try {
// Освобождаем необходимые ресурсы
} finally {
super.finalize();
}
}
Обратите внимание на реализацию метода finalize, на блок try/finally. Даже если освобождение ресурсов в блоке try прервется исключением, то независимо от этого будет вызван метод finalize наследуемого класса.
Далее я приведу еще один пример, отражающий защищенный вариант финализации.
public class Worker {
private final Object fg = new Object() {
@Override
protected void finalize() throws Throwable {
// Очищаем необходимые ресурсы, занятые объектом класса Worker
}
};
}
Такая реализация позволяет защититься от забывчивых программистов. Если программист переопределит в наследнике от класса Worker метод finalize и забудет вызвать реализацию суперкласса, то ничего страшного не произойдет. Финализация будет выполнена за счет внутреннего объекта. Джошуа Блох в своей книге Effective Java, назвал такой подход Finalizer Guardian.
Стоит предостеречь программистов, пишущих на C++, от использования finalize в качестве деструктора. В C++ весь код, связанный с освобождением памяти объекта, помещается в диструктор, который вызывается автоматически, когда объект выходит из области определения или когда удаляется объект, созданный динамически. А в Java нет такого понятия как деструктор, освобождением памяти связанной с объектом, занимается сборщик мусора.
Теперь пришло время затронуть тему сборки мусора, она тесно связана с методом finalize. Как правило, сборщик мусора активизируется при низком уровне свободной памяти, в этот момент он пытается отчистить память от неиспользуемых объектов. Но запуск GC еще не гарантирует то, что после очистки будет достаточно памяти. Если памяти недостаточно даже после очистки, JVM генерирует исключение OutOfMemoryError. Есть возможность явно попросить GC начать свою работу (Runtime.getRuntime().gc()), но это не гарантирует что GC начнет работу немедленно.
Прежде чем уничтожить объект, необходимо сообщить ему о том, что он более не используется и что необходимо освободить все свои ресурсы не связанные с памятью. Для это GC собирает все готовые к уничтожению объекты в определенную очередь и уже далее за работу принимается Finalizer, который берет из этой очереди объекты и вызывает у них метод finalize. После выполнения finalize, GC вновь проверит объект на доступность и если объект будет недостижим, то он может быть уничтожен. Но может так случиться, что объект, после выполнения финализации, снова станет доступным, в этом случае он не будет уничтожен. Но в следующий раз, когда GC вновь обнаружит, что данный объект более недостижим, он не попадет в очередь на финализацию и просто дождется своего удаления.
Не смотря на то, что спецификацией не гарантируется вызов метода finalize, все же есть способ который гарантирует финализацию каждого объекта. Следующий пример кода демонстрирует это.
public class Test { public static void main(String [] args) { Runtime.getRuntime().runFinalizersOnExit(true); for (int i = 0; i < 10; i++) {
new A();
}
System.exit(0);
}
}
class A {
@Override
protected void finalize() throws Throwable {
System.out.println("onFinalize");
}
}
На на консоль будет выведено 10 раз onFinalize, причем даже вызов exit не воспрепятствует этому. Но метод runFinalizersOnExit с версии 1.3 помечен как deprecated. Этот метод по сути небезопасен, в следствии его использования можно получить дедлок.
Необходимо отметить, что Финализацией занимается отдельный поток, имя которого Finalizer. Он последовательно вызывает метод finalize у каждого, стоящего в очереди на финализацию, объекта. А это значит, что если выполнение одного метода finalize застопорилось (или зациклилось), то до остальных дело вообще может не дойти. Причем объекты, готовые к финализации, весят в памяти, сжирая ресурсы. Рассмотрим следующий пример:
public class Test {
public static void main(String[] args) {
Random rnd = new Random(1000);
for (int i = 0; i < 100000; i++) {
new A(Integer.toHexString(rnd.nextInt()));
}
while (true) {
}
}
}
class A {
private String name;
public A(String str) {
this.name = str;
}
@Override
protected void finalize() throws Throwable {
System.out.println(
Thread.currentThread().getName() + " - " + name);
while (true) {
}
}
}
Если запустить на выполнение это код, то получим примерно следующее:
console > Finalizer - 396dde12
причем программа будет выполняться вечно и на консоль больше ничего не выведется.
Разберем пример... В методе main создаются 100 000 объектов, в какой то момент Finalizer берется за работу, но зацикливается на первом вызове finalize. Программа остается висеть и ни один finalize больше не выполнится, а все 100 000 объектов останутся в памяти. Если убрать зацикливание в методе finalize, то финализация будет выполнена для множества объектов, об этом будет свидетельствовать результат вывода на консоль.
Если цикл увеличить до 1 000 000 итераций, то программа в скором времени вообще умрет. При этом на консоль не выведется никакого стектрейса, все будет тихо и без какого либо шума. Единственное что может нам помочь разобраться, это код завершения JVM. В данном случае он будет равен 1, что свидетельствует об аварийном завершении программы.
Далее приведу еще парочку интересных скриншотов, на которых отражается "предсмертное" состояние JVM.
Есть еще один момент, который стоит затронуть. В классе RunTime есть метод runFinalization, как Вы дальше увидите, это очень интересный метод. В каждом экземпляре запущенной JVM присутствует один поток Finalizer, который занимается финализацией объектов. Но на самом деле, таких потоков может быть несколько. Вызывая метод runFinalization, Вы инициируете создание еще одного потока Finalizer-а. Потоки, запущенные с помощью метода runFinalization, будут иметь название Secondary finalizer и будут выполняться пока в очереди на финализацию будут находится объекты. Важно отметить, что runFinalization вернет управление только тогда, когда запущенный им Finalizer завершит свою работу, т.е. пока очередь на финализацию не опустеет. Следующий пример кода продемонстрирует все описанное выше.
public class Test { public static void main(String [] args) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) {
new A();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("before runFinalization");
System.runFinalization();
System.out.println("after runFinalization");
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
while (true) {
Thread.yield();
}
}
}
class A {
@Override
protected void finalize() throws Throwable {
System.out.println(Thread.currentThread().getName());
while (true) {
Thread.yield();
}
}
}
Если выполнить приведенный код, то по выводу на консоль можно будет увидеть, что сначала запустится штатный финализатор (Finalizer), а потом будет запущено еще 10 финализаторов (Secondary finalizer). При этом, на консоли мы некогда не увидим "after runFinalization". Это связано с тем, что метод runFinalization ждет пока созданный им поток завершит свою работу, что в данном примере никогда не произойдет.
Поток Finalizer всегда присутствует в запущенной JVM. Он находится в состоянии wait до того момента, пока не получит от GC команду "фас". В отличии от штатного Finalizer-а, который является потоком-демоном, Finalizer-ы созданные с помощью метода runFinalization являются обычными потоками.
Думаю, что пришло время подвести итоги. Применять метод finalize не безопасно, старайтесь не делать этого, кроме как в качестве подстраховки или для освобождения некритических внешних ресурсов! В тех случаях когда Вы решаете переопределить метод finalize, лучше применить подход Finalizer Guardian. Ну вот и все.
Продолжение следует, в следующей серии о clone...
Комментариев нет:
Отправить комментарий