22

22


Похоже, что поддержка метода finalize() была введена в язык, чтобы сделать возможными операции с памятью в стиле С, с привлечением нестандартных ме¬ханизмов выделения памяти. Это может произойти в основном при использо¬вании методов, предоставляющих способ вызова He-Java-кода из программы на Java. С и С++ пока являются единственными поддерживаемыми языками, но, так как для них таких ограничений нет, в действительности программа Java может вызвать любую процедуру или функцию на любом языке. Во внешнем коде можно выделить память вызовом функций С, относящихся к семейству malloc(). Если не воспользоваться затем функцией free(), произойдет «утечка» памяти. Конечно, функция free() тоже принадлежит к С и С++, поэтому придет¬ся в методе finalize() провести вызов еще одного «внешнего» метода.
После прочтения этого абзаца у вас, скорее всего, сложилось мнение, что ме¬тод finalize() используется нечасто1. И правда, это не то место, где следует про¬водить рутинные операции очистки. Но где же тогда эти обычные операции бу¬дут уместны?
Очистка — ваш долг
Для очистки объекта его пользователю нужно вызвать соответствующий метод в той точке, где эти завершающие действия по откреплению и должны осущест¬вляться. Звучит просто, но немного протйворечит традиционным представле¬ниям о деструкторах С++. В этом языке все объекты должны уничтожаться. Ес¬ли объект С++ создается локально (то есть в стеке, что невозможно в Java), то удаление и вызов деструктора происходит у закрывающей фигурной скобки, ограничивающей область действия такого объекта. Если же объект создается оператором new (как в Java), то деструктор вызывается при выполнении про¬граммистом оператора С++ delete (не имеющего аналога в Java). А когда про¬граммист на С++ забывает вызвать оператор delete, деструктор не вызывается и происходит «утечка» памяти, к тому же остальные части объекта не проходят необходимой очистки. Такого рода ошибки очень сложно найти и устранить, и они являются веским доводом в пользу перехода с С++ на Java.
Java не позволяет создавать локальные объекты — все объекты должны быть результатом действия оператора new. Но в Java отсутствует аналог оператора delete, вызываемого для разрушения объекта, так как сборщик мусора и без того выполнит освобождение памяти. Значит, в несколько упрощенном изложении можно утверждать, что деструктор в Java отсутствует из-за присутствия сбор¬щика мусора. Но в процессе чтения книги вы еще не раз убедитесь, что наличие сборщика мусора не устраняет необходимости в деструкторах или их аналогах. (И никогда не стоит вызывать метод finalize() непосредственно, так как этот подход не решает проблему.) Если же потребуется провести какие-то завер¬шающие действия, отличные от освобождения памяти, все же придется явно вызвать подходящий метод, выполняющий функцию деструктора С++, но это уже не так удобно, как встроенный деструктор.
Помните, что ни сборка мусора, ни финализация не гарантированы. Если виртуальная машина Java (Java Virtual Machine, JVM) далека от критической точки расходования ресурсов, она не станет тратить время на освобождение па¬мяти с использованием сборки мусора.
Условие «готовности»
В общем, вы не должны полагаться на вызов метода finalize() — создавайте от¬дельные «функции очистки» и вызывайте их явно. Скорее всего, finalize() при¬годится только в особых ситуациях нестандартного освобождения памяти, с ко¬торыми большинство программистов никогда не сталкивается. Тем не менее существует очень интересное применение метода finalize(), не зависящее от того, вызывается ли он каждый раз или нет. Это проверка условия готовности объекта.
В той точке, где объект становится ненужным — там, где он готов к проведе¬нию очистки, — этот объект должен находиться в состоянии, когда освобождение закрепленной за ним памяти безопасно. Например, если объект представляет открытый файл, то он должен быть соответствующим образом закрыт, перед тем как его «приберет» сборщик мусора. Если какая-то часть объекта не будет готова к уничтожению, результатом станет ошибка в программе, которую затем очень сложно обнаружить. Ценность finalize() в том и состоит, что он позволяет вам обнаружить такие ошибки, даже если и не всегда вызывается. Единожды проведенная финализация явным образом укажет на ошибку, а это все, что вам нужно.
Простой пример использования данного подхода:
//• i ni ti ali zati on/Termi nati onCondi ti on java
// Использование finalize() для выявления объекта,
// не осуществившего необходимой финализации
class Book {
boolean checkedOut = false,
Book(boolean checkout) {
checkedOut = checkout,
}
void checklnO {
checkedOut = false;
}
public void finalizeO { if(checkedOut)
System out println("Ошибка. checkedOut"); // Обычно это делается так-
// Super.finalize(), // Вызов версии базового класса
}
}
public class TerminationCondition {
public static void main(String[] args) { Book novel = new Book(true); // Правильная очистка- novel.checkln(),
// Теряем ссылку, забыли про очистку new Book(true);
// Принудительная сборка мусора и финализация System gc().
}
} /* Output
Ошибка checkedOut
* ///•-
«Условие готовности» состоит в том, что все объекты Book должны быть «сняты с учета» перед предоставлением их в распоряжение сборщика мусора, но в методе main() программист ошибся и не отметил один из объектов Book. Если бы в методе finalize() не было проверки на условие «готовности», такую оплошность было бы очень сложно обнаружить.
Заметьте, что для проведения принудительной финализации был использо¬ван метод System.gc(). Но даже если бы его не было, с высокой степенью вероят¬ности можно сказать, что «утерянный» объект Book рано или поздно будет об¬наружен в процессе исполнения программы (в этом случае предполагается, что программе будет выделено столько памяти, сколько нужно, чтобы сборщик мусора приступил к своим обязанностям).
Обычно следует считать, что версия finalize() базового класса делает что-то важное, и вызывать ее в синтаксисе super, как показано в Book.finalize(). В дан¬ном примере вызов закомментирован, потому что он требует обработки исклю¬чений, а эта тема нами еще не рассматривалась.
Как работает сборщик мусора
Если ранее вы работали на языке программирования, в котором выделение мес¬та для объектов в куче было связано с большими издержками, то вы можете предположить, что и в Java механизм выделения памяти из кучи для всех дан¬ных (за исключением примитивов) также обходится слишком дорого. Однако в действительности использование сборщика мусора дает немалый эффект по ускорению создания объектов. Сначала это может звучать немного странно — освобождение памяти сказывается на ее выделении — но именно так работают некоторые JVM, и это значит, что резервирование места для объектов в куче Java не уступает по скорости выделению пространства в стеке в других языках.
Представтьте кучу языка С++ в виде лужайки, где каждый объект «застол¬бил» свой собственный участок. Позднее площадка освобождается для повтор¬ного использования. В некоторых виртуальных машинах Java куча выглядит совсем иначе; она скорее похоже на ленту конвейера, которая передвигается вперед при создании нового объекта. А это значит, что скорость выделения хра¬нилища для объекта оказывается весьма высокой. «Указатель кучи» просто пе¬редвигается вперед в «невозделанную» территорию, и по эффективности этот процесс близок к выделению памяти в стеке С++. (Конечно, учет выделенного пространства сопряжен с небольшими издержками, но их никоим образом нельзя сравнить с затратами, возникающими при поиске свободного блока в па¬мяти.)
Конечно, использование кучи в режиме «ленты конвейера» не может про¬должаться бесконечно, и рано или поздно память станет сильно фрагментиро- вана (что заметно снижает производительность), а затем и вовсе исчерпается. Как раз здесь в действие вступает сборщик мусора; во время своей работы он компактно размещает объекты кучи, как бы смещая «указатель кучи» ближе к началу «ленты», тем самым предотвращая фрагментацию памяти. Сборщик мусора реструктуризует внутреннее расположение объектов в памяти и по¬зволит получить высокоскоростную модель кучи для резервирования памяти.
Чтобы понять, как работает сборка мусора в Java, необходимо узнать, как устроены реализации сборщиков мусора (СМ) в других системах. Простой, но медленный механизм СМ называется подсчетом ссылок. С каждым объектом хранится счетчик ссылок на него, и всякий раз при присоединении новой ссыл¬ки к объекту этот счетчик увеличивается. Каждый раз при выходе ссылки из области действия или установке ее значения в null счетчик ссылок уменьша¬ется. Таким образом, подсчет ссылок создает небольшие, но постоянные из¬держки во время работы вашей программы. Сборщик мусора перебирает объект за объектом списка; обнаружив объект с нулевым счетчиком, он освобождает ресурсы, занимаемые этим объектом. Но существует одна проблема — если объ¬екты содержат циклические ссылки друг на друга, их счетчики ссылок не обну¬ляются, хотя на самом деле объекты уже являются «мусором». Обнаружение таких «циклических» групп является серьезной работой и отнимает у сборщика мусора достаточно времени. Подсчет ссылок часто используется для объясне¬ния принципов процесса сборки мусора, но, судя по всему, он не используется ни в одной из виртуальных машин Java.
В более быстрых схемах сборка мусора не зависит от подсчета ссылок. Вме¬сто этого она опирается на идею, что любой существующий объект прослежива¬ется до ссылки, находящейся в стеке или в статической памяти. Цепочка про¬верки проходит через несколько уровней объектов. Таким образом, если начать со стека и статического хранилища, мы обязательно доберемся до всех исполь¬зуемых объектов. Для каждой найденной ссылки надо взять объект, на который она указывает, и отследить все ссылки этого объекта; при этом выявляются другие объекты, на которые они указывают, и так далее, пока не будет провере¬на вся инфраструктура ссылок, берущая начало в стеке и статической памяти. Каждый объект, обнаруженный в ходе поиска, все еще используется в системе. Заметьте, что проблемы циклических ссылок не существует — такие ссылки просто не обнаруживаются, и поэтому становятся добычей сборщика мусора ав¬томатически.
В описанном здесь подходе работает адаптивный механизм сбора мусора, при котором JVM обращается с найденными используемыми объектами соглас¬но определенному варианту действий. Один из таких вариантов называется ос- тановить-и-копировать. Смысл термина понятен: работа программы временно приостанавливается (эта схема не поддерживает сборку мусора в фоновом ре¬жиме). Затем все найденные «живые» (используемые) объекты копируются из одной кучи в другую, а «мусор» остается в первой. При копировании объек¬тов в новую кучу они размещаются в виде компактной непрерывной цепочки, высвобождая пространство в куче {и позволяя удовлетворять заказ на новое хранилище простым перемещением указателя).

Report Page