Главная » Java, Советы » Остерегайтесь методов flnalize

0

 

Методы finalize непредсказуемы, часто опасны и, как правило, не нужны. Их использование может привести к странному поведению программы, низкой производительности и проблемам с переносимостью. Метод finalize имеет лишь несколько областей применения (см. ниже), а главное правило таково: следует избегать методов finalize.

Программистов, пишущих на С++, следует предостеречь в том, что нельзя рассматривать методы finalize как аналог деструкторов в С++. В С++ деструктор _ это обычный способ утилизации ресурсов, связанных с объектом, обязательное дополнение к конструктору. В языке программирования java, когда объект становится недоступен, очистку связанной с ним памяти осуществляет сборщик мусора. Со стороны же программиста никаких специальных действий не требуется. В С++ деструкторы используются для освобождения не только памяти, но и других ресурсов системы. В языке программирования java для этого обычно применяется блок  try-finally.

Нет гарантии, что метод finalize будут вызван немедленно [JLS, 12.6]. С момента, когда объект становится недосТупен, и до момента выполнения метода finalize может пройти сколь угодно длительное время. Это означает, что с помощью метода finalize нельзя выполнять никаких операций, критичных по времени. Например, будет серьезной ошибкой ставить процедуру закрытия открытых файлов в зависимость от метода finalize, поскольку дескрипторы открытых файлов – ресурс ограниченный. Если из-за того, что jVM медлит с запуском методов finalize, открытыми будут оставаться много файлов, программа может завершиться с ошибкой, поскольку ей не удастся открыть новые файлы.

Частота запуска методов finalize в первую очередь определяется алгоритмом сборки мусора, который существенно меняется от одной реализации jVM к другой. Точно так же может меняться и поведение программы, работа которой зависит от частоты вызова методов finalize. Вполне возможно, что программа будет превосходно работать с jVM, на которой проводится ее тестирование, а затем позорно даст сбой на JVM, которую предпочитает ваш самый важный заказчик.

Запоздалый вызов методов finalize – не только теоретическая проблема. Создав ДЛЯ какого-либо класса метод finalize, в ряде случаев можно спровоцировать произвольную задержку при удалении его экземпляров. Один мой коллега недавно отлаживал приложение СИI, которое было рассчитано на длительное функционирование, но таинственно умирало с ошибкой OutOfMemoryError. Анализ показал, что в момент смерти  приложение в очереди на удаление стояли тысячи графических объектов, ждавших лишь вызова метода finalize и утилизации. К несчастью, поток утилизации выполнялся с меньшим приоритетом, чем другой поток того же приложения, а потому удаление объектов не могло осуществиться в том же темпе, в каком они становились доступны для удаления. Спецификация языка Java не определяет, в каком из потоков

 

будут выполняться методы finalize. Поэтому нет иного универсального способа предотвратить проблемы такого рода, кроме как воздерживаться от использования методов finalize.

Спецификация языка Java не только не дает поручительства, что методы finalize будут вызваны быстро, она не гарантирует, что они вообще будут вызваны. Вполне возможно, что программа завершится, так и не вызвав метод finalize для некоторых объектов, ставших недоступными. Следовательно, обновление критического фиксируемого (persistent) состояния не должно зависеть от метода finalize. Например, ставить освобождение фиксируемой блокировки разделяемого ресурса, такого как база данных, в зависимость от метода finalize – верный способ привести всю вашу распределенную систему к сокрушительному краху.

Не соблазняйтесь методами System.gc и System.runFinalization. Они могут повысить вероятность запуска утилизации, но не гарантируют этого. Единственные методы, требующие гарантированного удаления,- это System.runFinalizersOnExit и его вредный близнец Runtime.runFinalizersOnExit. Эти методы некорректны и признаны устаревшими.

Стоит обратить внимание еще на один момент: если в ходе утилизации возникает необработанная исключительная ситуация (exception), она игнорируется, а утилизация этого объекта прекращается [JLS, 12.6]. Необработанная исключительная ситуация может оставить объект в испорченном состоянии. И если другой поток попытается воспользоваться испорченным объектом, результат в определенной мере может быть непредсказуем. Обычно необработанная исключительная ситуация завершает поток и выдает распечатку стека, однако в методе finalize этого не происходит; он даже не выводит предупреждений.

Так чем же заменить метод finalize для класса, чьи объекты инкапсулируют ресурсы, требующие завершения, такие как файлы или потоки? Создайте метод для прямого завершения и потребуйте, чтобы клиенты класса вызывали этот метод для каждого экземпляра, ,когда он им больше не нужен. Стоит упомянуть об одной детали: экземпляр сам должен следить за тем, был ли он завершен. Метод прямо го завершения должен делать запись в некое м закрытом поле о том, что объект более не является действительным. Остальные методы класса должны проверять это поле и инициировать исключительную ситуацию IllegalStateException, если их вызывают после того, как данный объект был завершен.

Типичный пример метода прямого завершения – метод close в InputStгеаm и OutputStгеаm. Еще один пример – метод саnсеl из jауа.util.Timer, который нужным образом меняет состояние объекта, заставляя поток (thread), связанный с экземпляром Timer, аккуратно завершить свою работу. Среди примеров из пакета jауа.awt – Graphics.dispose и Window.dispose. На эти методы редко обращают внимание, что сказывается на производительности программы. То же самое касается метода Image. flush, который освобождает все ресурсы, связанные с экземпляром Image, но оставляет последний в таком состоянии, что его еще можно использовать, выделив вновь необходимые ресурсы.

 

20Методы прямого завершения часто используются в сочетании с конструкцией try-finally для обеспечения гарантированного завершения. Вызов метода прямого завершения из оператора finally гарантирует, что он будет выполнен, если даже при работе с объектом возникнет исключительная ситуация:

// Блок try-finally гарантирует вызов метода завершения

Foo foo = new Foo( … );

tгу {

// Делаем то, что необходимо сделать с foo

}finally {

foo.terminate(); // Метод прямого Завершения

}

Зачем же тогда вообще нужны методы finalize? У них есть два приемлемых применения. Первое – они выступают в роли "страховочной сетки" в том случае, если владелец объекта забывает вызвать метод прямо го завершения. Нет гарантии, что метод finalize будет вызван своевременно, однако в тех случаях (будем надеяться, редких), когда клиент не выполняет свою часть соглашения, т. е. не вызывает метод прямого завершения, критический ресурс лучше ‘освободить поздно, чем никогда. Три класса, представленных как пример использования метода прямого завершения (InputStream, OutputStream и Тiтeг), тоже имеют методы finalize, которые применяется в качестве страховочной сетки, если соответствующие методы завершения не были вызваны.

Другое приемлемое применение методов finalize связано с объектами, имеющими "местных партнеров" (native peers). Местный партнер – это местный объект (native object), к которому обычный объект обращается через машинно-зависимые методы. Поскольку местный партнер не является обычным объектом, сборщик мусора о нем не знает, и когда утилизируется обычный партнер, утилизировать местного партнера он не может. Метод finalize является приемлемым посредником для решения этой задачи при условии, что местный партнер не содержит критических ресурсов. Если же местный партнер содержит ресурсы, которые необходимо освободить немедленно, данный класс должен иметь метод прямого завершения. Этот метод завершения обязан делать все, что необходимо для освобождения соответствующего критического ресурса. Метод завершения может быть машинно-зависимым методом либо вызывать таковой.

Важно отметить, что здесь нет автоматического связывания методов finalize ("finalizer chaining"). Если в классе (за исключениемОЬjесt) есть метод finalize, но в подклассе он был переопределен, то метод finalize в подклассе должен вызывать. Метод finalize, из суперкласса. Вы должны завершить подкласс в блоке try, а затем в Соответствующем блоке finally вызвать метод finalize суперкласса. Тем самым

 

гарантируется, что метод finalize суперкласса будет вызван, даже если при завершении подкласса инициируется исключительная ситуация, и наоборот:

 

// Ручное связывание метода finalize

protected void finalize() throws Throwable try {

//’Ликвидируем состояние подкласса

finally {

super.f1nal1ze();

}

}

Если разработчик подкласса переопределяет метод finalize суперкласса, но забывает вызвать его "вручную" (или не делает этого из вредности), метод finalize суперкласса так и не будет вызван. Защититься от такого беспечного или вредного подкласса можно ценой создания некоего дополнительного объекта для каждого объекта, подлежащего утилизации. Вместо того чтобы размещать метод finalize в классе, требующем утилизации, поместите его в анонимный класс (статья 18), единственным назначением которого будет утилизация соответствующего экземпляра. для каждого экземпляра контролируемого класса создается единственный экземпляр анонимного класса, называемый хранителем утилизации (fjnalizer guardian). Контролируемый экземпляр содержит в закрытом экземпляре поля единственную в системе ссылку на хранителя утилизации. Таким образом, хранитель утилизации становится доступен для удаления в момент утилизации контролируемого им экземпляра. Когда хранитель утилизируется, он выполняет процедуры, необходимые для ликвидации контролируемого им экземпляра, как если бы его метод finalize был методом контролируемого класса:

 

// Идиома хранителя утилизации (Finalizer Guardian)

public class Foo {

// Единственная задача этого объекта – утилизировать

// внешний объект Foo

private final Object finalizerGuardian = new Object() protected void finalize() throws Throwable

// Утилизирует внешний объект Foo

} ;

// Остальное опущено

}

Заметим, что у открытого класса Foo нет метода finalize (за исключением·тривиального, унаследованного от класса Object), а потому не важно, был ли в методе f1nalize подкласса вызов метода super.finalize или нет. Возможность использования этой 22

методики следует рассмотреть для каждого открытого расширяемого класса, имеющего метод finalize.

Подведем итоги. Не применяйте методы finalize, кроме как в качестве страховочной сетки или для освобождения некритических местных ресурсов. В тех редких случаях, когда вы должны использовать метод finalize, не забывайте делать вызов Super.finalize. И последнее: если вам необходимо связать метод finalize с открытым классом без модификатора finaI, подумайте о применении хранителя утилизации, чтобы быть уверенным в том, что утилизация будет выполнена, даже если в подклассе в методе finalize не будет вызова super.finalize. 23

Источник: Джошуа Блох, Java TM Эффективное программирование, Издательство «Лори»

По теме:

  • Комментарии