Невже переважаючий Object.finalize () дійсно поганий?


34

Основні два аргументи проти переосмислення Object.finalize():

  1. Ви не можете вирішити, коли він називається.

  2. Він може взагалі не зателефонувати.

Якщо я правильно це розумію, я не думаю, що це досить вагомі причини, щоб Object.finalize()так сильно ненавидіти .

  1. Саме реалізація VM і GC визначають, коли саме потрібний час для розміщення об'єкта, а не розробник. Чому важливо вирішити, коли вас Object.finalize()дзвонять?

  2. Як правило, і виправте мене, якщо я помиляюся, єдиний час, коли мені Object.finalize()не дзвонять, - це коли програма припиняється до того, як GC отримає шанс запуститись. Однак об’єкт все-таки перемістився, коли процес застосування з ним припинився. Тож Object.finalize()не дзвонили, бо його не потрібно було викликати. Чому б піклувався розробник?

Кожен раз, коли я використовую об'єкти, які мені доводиться закривати вручну (наприклад, ручки файлів та з'єднання), я дуже засмучуюся. Мені потрібно постійно перевіряти, чи об’єкт має реалізацію close(), і я впевнений, що я пропустив кілька дзвінків до нього в певні моменти минулого. Чому не просто простіше і безпечніше просто залишити його ВМ та ГК, щоб розпоряджатися цими об'єктами, розміщуючи close()реалізацію Object.finalize()?


1
Також зауважте: як і багато API з епохи Java 1.0, семантика потоків finalize()трохи заплутана. Якщо ви коли-небудь реалізовуєте це, переконайтеся, що він є безпечним для потоків стосовно всіх інших методів того ж об’єкта.
billc.cn

2
Коли ви чуєте, як люди говорять, що фіналізатори погані, це не означає, що ваша програма перестане працювати, якщо у вас є; вони означають, що вся ідея доопрацювання є досить марною.
користувач253751

1
+1 до цього питання. Більшість відповідей нижче зазначає, що такі ресурси, як дескриптори файлів, обмежені і що їх слід збирати вручну. Те саме було / стосується пам’яті, тому якщо ми приймаємо певну затримку в колекції пам’яті, то чому б не прийняти її для дескрипторів файлів та / або інших ресурсів?
mbonnin

Адресуючи свій останній абзац, ви можете залишити його Java для обробки закритих речей, таких як ручки файлів та з'єднання з дуже невеликими зусиллями з вашого боку. Скористайтеся блоком «випробування ресурсів» - це згадується ще кілька разів у відповідях та коментарях, але я думаю, що тут варто поставити його. Підручник Oracle для цього можна знайти на docs.oracle.com/javase/tutorial/essential/exceptions/…
Jeutnarg

Відповіді:


45

На мій досвід, є одна і єдина причина для переосмислення Object.finalize(), але це дуже вагома причина :

Розмістити код реєстрації помилок, finalize()який повідомляє вас, якщо ви коли-небудь забудете посилатися close().

Статичний аналіз може зафіксувати лише упущення у тривіальних сценаріях використання, а попередження компілятора, згадані в іншій відповіді, мають таке спрощене уявлення про речі, що вам насправді потрібно вимкнути їх, щоб зробити щось нетривіальне. (У мене ввімкнено набагато більше попереджень, ніж будь-який інший програміст, про який я знаю або коли-небудь чув, але у мене немає тупого попередження.)

Фіналізація може здатися хорошим механізмом для того, щоб ресурси не залишалися нерозкритими, але більшість людей бачать це абсолютно неправильно: вони вважають це альтернативним механізмом резервного резервування, захисником "другого шансу", який автоматично врятує день, утилізуючи ресурси, які вони забули. Це мертве неправильно . Має бути лише один спосіб зробити будь-яку задану річ: або ти завжди все закриваєш, або остаточне завершення завжди все закриває. Але оскільки доопрацювання є ненадійним, доопрацювання не може бути.

Отже, існує така схема, яку я називаю Обов'язковим видаленням , і вона передбачає, що програміст відповідає за завжди явне закриття всього, що реалізує Closeableабо AutoCloseable. (Оператор спробу використання ресурсів все ще вважається явним закриттям.) Звичайно, програміст може забути, тож саме тут вступає в дію фіналізація, але не як чарівна фея, яка в кінцевому підсумку магічно зробить все правильно: Якщо фіналізація виявиться що close()не посилалося, це не такнамагатися викликати це саме тому, що з (з математичною визначеністю) з'являться орди програмістів n00b, які будуть покладатися на нього, щоб виконати ту роботу, яку вони занадто ліниві або занадто роздуми робили. Отже, при обов'язковому видаленні, коли доопрацювання виявить, що close()не було викликано, воно записує яскраво-червоне повідомлення про помилку, повідомляючи програмісту великими великими великими літерами, щоб виправити його, його речі.

Як додаткову вигоду, ходить чутка, що "JVM проігнорує тривіальний метод finalize () (наприклад, той, який просто повертається, нічого не роблячи, як той, який визначений у класі Object)", тому при обов'язковому видаленні ви можете уникнути всієї завершення накладні витрати у всій вашій системі ( див. відповідь аліпа для отримання інформації про те, наскільки жахливі ці накладні витрати), кодуючи ваш finalize()метод таким чином:

@Override
protected void finalize() throws Throwable
{
    if( Global.DEBUG && !closed )
    {
        Log.Error( "FORGOT TO CLOSE THIS!" );
    }
    //super.finalize(); see alip's comment on why this should not be invoked.
}

Ідея цього полягає в тому, що Global.DEBUGце static finalзмінна, значення якої відоме під час компіляції, тож якщо саме falseтоді компілятор взагалі не видасть жодного коду для всього ifоператора, що зробить це тривіальним (порожнім) фіналізатором, що, у свою чергу означає, що до вашого класу поводиться так, ніби він не має фіналізатора. (У C # це було б зроблено з приємним #if DEBUGблоком, але що ми можемо зробити, це java, де ми платимо очевидну простоту в коді з додатковими накладними витратами в мозку.)

Більше про обов'язкове знешкодження, з додатковою дискусією щодо утилізації ресурсів у крапці Net, тут: michael.gr: Обов’язкове утилізація проти гидоти "Утилізація-розпорядження"


2
@MikeNakis Не забувайте, Closeable визначається як нічого не робити, якщо викликається вдруге: docs.oracle.com/javase/7/docs/api/java/io/Closeable.html . Я визнаю, що іноді я записував попередження, коли мої заняття два рази закриваються, але технічно ви навіть цього не повинні робити. У технічному відношенні виклик .close () на Clovable не один раз цілком дійсний.
Патрік М

1
@usr все зводиться до того, чи довіряєте ви тестуванню чи не довіряєте тестуванню. Якщо ви не довіряєте своєму тестування, звичайно, йти вперед і страждати над головою фіналізації для також close() , на всякий випадок. Я вважаю, що якщо моє тестування не буде довіряти, я краще не відпускати систему у виробництво.
Майк Накіс

3
@Mike, щоб if( Global.DEBUG && ...конструкція працювала так, що JVM буде ігнорувати finalize()метод як тривіальний, його Global.DEBUGпотрібно встановити під час компіляції (на відміну від введеного тощо), тож наступне буде мертвим кодом. Виклику за super.finalize()межами блоку if, якщо також достатньо, щоб JVM вважав його нетривіальним (незалежно від значення Global.DEBUG, принаймні, на HotSpot 1.8), навіть якщо також #finalize()тривіальне значення для суперкласу !
alip

1
@Mike Боюся, що це так. Я перевірив це (злегка модифікована версія) тесту в статті, до якої ви посилаєтесь , і багатослівний вихід GC (разом із напрочуд низькою продуктивністю) підтверджує, що об'єкти скопійовані в космос вижилого / старого покоління і для отримання потрібної повної маси GC позбутися.
аліп

1
Те, що часто не помічається, - це ризик отримання раніше об'єктів, ніж очікувалося, що робить звільнення ресурсів у фіналізаторі дуже небезпечною дією. Перед Java 9 єдиний спосіб переконатися, що фіналізатор не закриває ресурс, поки він все ще використовується, - це синхронізація на об'єкті як у фіналізаторі, так і в методах (іх) за допомогою ресурсу. Ось чому це працює в java.io. Якщо така безпека ниток не була в списку бажань, вона додається до накладних витрат, викликаних finalize()
Holger

28

Кожен раз, коли я використовую об'єкти, які мені доводиться закривати вручну (наприклад, ручки файлів та з'єднання), я дуже засмучуюся. [...] Чому не просто простіше і безпечніше просто залишити його ВМ та ГК, щоб утилізувати ці об'єкти, поставивши в close()реалізацію Object.finalize()?

Оскільки ручки та з'єднання файлів (тобто дескриптори файлів у системах Linux та POSIX) є досить обмеженим ресурсом (ви можете обмежитися 256 ними в деяких системах або 16384 в деяких інших; див. Setrlimit (2) ). Немає гарантії, що ГК буде викликатися досить часто (або в потрібний час), щоб уникнути виснаження таких обмежених ресурсів. І якщо GC не буде викликано недостатньо (або завершення не буде виконано в потрібний час), ви досягнете цієї (можливо, низької) межі.

Фіналізація - це "найкращі зусилля" у Спільному проекті. Він може не називатися або називатися досить пізно ... Зокрема, якщо у вас багато оперативної пам’яті або якщо ваша програма не виділяє багато об’єктів (або якщо більшість з них гине, перш ніж переадресуватися на якийсь досить старий покоління, що копіює покоління GC), GC можна назвати досить рідко, і остаточні завершення можуть працювати не дуже часто (або навіть не можуть працювати зовсім).

Тому closeдескриптори файлів явно, якщо можливо. Якщо ви боїтесь їх витоку, використовуйте доопрацювання як додатковий захід, а не як основний.


7
Я додав би, що закриття потоку до файлу або сокета зазвичай його промиває. Якщо залишити потоки відкритими, без необхідності збільшується ризик втрати даних, якщо живлення вимкнеться, з'єднання впаде (це також є ризиком для файлів, доступ до яких здійснюється через мережу) тощо.

2
Насправді, хоча кількість дозволених файлів-скриптерів може бути низькою, це не є дійсно клейким моментом, тому що можна використовувати це як сигнал для GC принаймні. Дійсно проблемна річ: а) абсолютно непрозорість для GC, скільки ресурсів, якими не керує GC, висять на ній, і b) багато з цих ресурсів є унікальними, тому інші можуть бути заблоковані або відмовлені.
Дедуплікатор

2
І якщо ви залишите файл відкритим, він може заважати комусь іншим користуватися ним. (Переглядач Windows 8 XPS, я дивлюся на вас!)
Лорен Печтел

2
"Якщо ви боїтесь витоку [дескриптори файлів], використовуйте доопрацювання як додатковий захід, а не як основний." Це твердження для мене звучить риба. Якщо ви добре спроектували свій код, чи справді слід ввести зайвий код, який поширює очищення по декількох місцях?
mucaho

2
@BasileStarynkevitch Ваша думка полягає в тому, що в ідеальному світі так, надмірність погана, але на практиці, де ви не можете передбачити всі відповідні аспекти, краще бути безпечним, ніж вибачте?
mucaho

13

Погляньте на справу так: вам слід писати лише код, який (а) правильний (інакше ваша програма явно неправильна) та (б) необхідна (інакше ваш код занадто великий, що означає більше необхідної оперативної пам'яті, більше циклів витрачено марні речі, більше зусиль для її розуміння, більше часу витрачається на його підтримку тощо тощо)

Тепер подумайте, що ви хочете зробити у фіналізаторі. Або це необхідно. У такому випадку ви не можете поставити його у фіналізатор, тому що ви не знаєте, чи він буде викликаний. Це недостатньо добре. Або це не обов’язково - тоді не варто писати це в першу чергу! У будь-якому випадку, введення його у фіналізатор - це неправильний вибір.

(Зверніть увагу, що названі вами приклади, як-от закриття потоків файлів, виглядають так, ніби вони насправді не потрібні, але вони є. Це просто так, поки ви не досягнете межі для відкритих файлових файлів у вашій системі, ви не помітите, що ваш код невірний. Але це обмеження є особливістю операційної системи і , отже , ще більш непередбачуваним , ніж політики JVM для фіналізатор, так що на насправді, на самому ділі це важливо , щоб не витрачати дескриптори файлів.)


Якщо я повинен лише написати код, який є "необхідним", то чи слід уникати всіх стилів у всіх моїх програмах GUI? Безумовно, нічого з цього не потрібно; графічні інтерфейси працюватимуть прекрасно без стилізації, вони просто виглядатимуть жахливо. Щодо фіналізатора, можливо, потрібно щось зробити, але все-таки добре, щоб помістити його у фіналізатор, оскільки фіналізатор буде викликаний під час об'єкта GC, якщо він взагалі є. Якщо вам потрібно закрити ресурс, зокрема, коли він готовий до вивезення сміття, тоді оптимізатором є оптимальний. Або програма припиняє звільнення вашого ресурсу, або виклик фіналізатора.
Крьов

Якщо у мене є об'єм оперативної пам'яті, важкий, дорогий для створення, закритий об'єкт, і я вирішу слабко посилатися на нього, щоб його можна було очистити, коли це не потрібно, тоді я міг би використовувати його finalize()для закриття у випадку, якщо цикл gc дійсно потребує звільнення ОЗП. В іншому випадку я б зберігав об'єкт в оперативній пам’яті, замість того, щоб регенерувати і закривати його щоразу, коли мені потрібно його використовувати. Звичайно, ресурси, які він відкриває, не будуть звільнені, поки об'єкт не буде GCed, коли це може бути, але мені, можливо, не потрібно буде гарантувати, що мої ресурси будуть звільнені в певний час.
Крьов

8

Однією з найважливіших причин не покладатися на фіналізатори є те, що більшість ресурсів, які можуть спокусити очистити у фіналізаторі, дуже обмежені. Збір сміття працює лише кожен так часто, оскільки проходження посилань на визначення того, чи можна щось випустити, коштує дорого. Це означає, що це може «пройти деякий час», перш ніж ваші об’єкти фактично будуть знищені. Наприклад, якщо у вас є багато об’єктів, що відкривають короткочасні підключення до бази даних, наприклад, випуск фіналізаторів для очищення цих з'єднань може вичерпати пул зв’язків, поки він чекає, коли збирач сміття остаточно запустить і звільнить готові з'єднання. Потім, через очікування, ви закінчуєтесь із великим відставанням запитів у черзі, які швидко знову вичерпують пул з'єднань. Це '

Крім того, використання пробних ресурсів робить закриття "закритих" об'єктів після завершення легко зробити. Якщо ви не знайомі з цією конструкцією, пропоную вам перевірити її: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html


8

Окрім того, чому залишати його фіналізатором для випуску ресурсів, як правило, погана ідея, доопрацьовувані об'єкти надходять з накладними витратами.

З теорії та практики Java: Збір сміття та продуктивність (Брайан Гец), Фіналізатори - це не ваш друг :

Об'єкти з фіналізаторами (ті, у яких є нетривіальний метод finalize ()) мають значні накладні витрати в порівнянні з об'єктами без фіналізаторів, і їх слід використовувати ощадливо. Закінчувані об'єкти розподіляють як повільніше, так і повільніше збирати. Під час розподілу JVM повинен реєструвати будь-які доопрацьовані об'єкти у сміттєзбірнику, і (принаймні, у впровадженні HotSpot JVM) доопрацьовані об’єкти повинні слідувати повільнішому шляху розподілу, ніж більшість інших об'єктів. Так само повільніше збирати доопрацьовані об'єкти. Потрібно щонайменше два цикли вивезення сміття (в кращому випадку), перш ніж фіналізований об’єкт може бути відкликаний, і сміттєзбірник повинен зробити додаткову роботу, щоб викликати фіналізатор. Результат - це більше часу, витраченого на розподіл та збирання предметів, і більший тиск на сміттєзбірник, тому що пам'ять, яка використовується недосяжними фіналізованими об'єктами, зберігається довше. Поєднайте це з тим, що фіналізатори не гарантовано працюють у будь-який передбачуваний часовий проміжок або навіть взагалі, і ви можете бачити, що існує порівняно мало ситуацій, для яких доопрацювання є правильним інструментом для використання.


Відмінний момент. Дивіться мою відповідь, яка дає спосіб уникнути накладних витрат finalize().
Майк Накіс

7

Моя (найменш) улюблена причина уникнення Object.finalizeполягає не в тому, що об’єкти можуть бути доопрацьовані після того, як ви цього очікуєте, але вони можуть бути доопрацьовані ще до того, як ви могли цього очікувати. Проблема полягає не в тому, що об'єкт, який все ще знаходиться в області застосування, може бути доопрацьований до виходу області, якщо Java вирішить, що він більше недоступний.

void test() {
   HasFinalize myObject = ...;
   OutputStream os = myObject.stream;

   // myObject is no-longer reachable at this point, 
   // even though it is in scope. But objects are finalized
   // based on reachability.
   // And since finalization is on another thread, it 
   // could happen before or in the middle of the write .. 
   // closing the stream and causing much fun.
   os.write("Hello World");
}

Дивіться це питання для більш детальної інформації. Ще веселіше те, що це рішення може бути прийняте лише після того, як починається оптимізація гарячих точок, що робить це болісним для налагодження.


1
Вся справа в тому, що HasFinalize.streamсам по собі повинен бути окремо завершеним об'єктом. Тобто доопрацювання HasFinalizeне повинно завершуватися чи намагатися очистити stream. Або якщо треба, то це має зробити streamнедоступним.
ацелент


4

Мені потрібно постійно перевіряти, чи має об’єкт реалізація close (), і я впевнений, що я пропустив кілька дзвінків до нього в певні моменти минулого.

У програмі Eclipse я отримую попередження кожного разу, коли забуваю закрити щось, що реалізує Closeable/ AutoCloseable. Я не впевнений, чи це Eclipse, чи це частина офіційного компілятора, але ви можете розглянути можливість використання аналогічних інструментів статичного аналізу, щоб допомогти вам. Наприклад, FindBugs може допомогти вам перевірити, чи забули ви закрити ресурс.


1
Гарна ідея згадування AutoCloseable. Це робить мозок-мертвим легко керувати ресурсами за допомогою тестування ресурсів. Це зводить нанівець декілька аргументів у питанні.

2

До вашого першого питання:

Саме реалізація VM і GC визначають, коли саме потрібний час для розміщення об'єкта, а не розробник. Чому важливо вирішити, коли вас Object.finalize()дзвонять?

Що ж, JVM визначить, коли вдало повернути сховище, виділене для об'єкта. Це не обов'язково finalize()має відбутися час, коли очищення ресурсу, який ви хочете виконати , має відбутися. Це проілюстровано у запитанні "finalize (), що викликається сильнодоступним об'єктом у Java 8" . Там close()метод був викликаний finalize()методом, поки спроба зчитувати з потоку тим самим об'єктом ще триває. Отже, крім загальновідомої можливості, яка finalize()закликає шлях до пізнього, існує ймовірність, що вона зателефонує занадто рано.

Передумова вашого другого питання:

Як правило, і виправте мене, якщо я помиляюся, єдиний час, коли мені Object.finalize()не дзвонять, - це коли програма припиняється до того, як GC отримає шанс запуститись.

просто неправильно. Зокрема, не існує жодної вимоги до того, що СВМ підтримує завершення. Ну, це не зовсім неправильно, оскільки ви все ще можете інтерпретувати це як "додаток припинено до того, як відбудеться його завершення", припускаючи, що ваша заявка коли-небудь припиниться.

Але зауважте, невелика різниця між "GC" вашого оригінального твердження та терміном "остаточне завершення". Збір сміття відрізняється від завершення. Як тільки управління пам'яттю виявить, що об’єкт недоступний, він може просто повернути його простір, якщо він не має спеціального finalize()методу, або доопрацювання просто не підтримується, або він може залучати об'єкт для доопрацювання. Таким чином, завершення циклу вивезення сміття не означає, що виконуються фіналізатори. Це може статися пізніше, коли чергу буде оброблена, або взагалі ніколи.

Цей момент також є причиною того, що навіть для JVM з підтримкою доопрацювання покладатися на очищення ресурсів небезпечно. Збір сміття є частиною управління пам’яттю і, отже, викликається потребами пам’яті. Можливо, що збирання сміття ніколи не працює, тому що достатньо пам’яті протягом усього часу роботи (ну, це все ще вписується в „опис програми припинено до того, як GC отримає шанс запустити” якось). Можливо також, що GC дійсно працює, але після цього достатньо відновленої пам'яті, тому черга на фіналізатор не обробляється.

Іншими словами, власні ресурси, керовані таким чином, все ще є чужими для управління пам’яттю. Хоча гарантовано, що анкета OutOfMemoryErrorбуде викинута лише після достатньої спроби звільнити пам'ять, це не стосується власних ресурсів та доопрацювання. Можливо, що відкриття файлу не вдається через недостатню кількість ресурсів, а в черзі на завершення повно об'єктів, які могли б звільнити ці ресурси, якщо фіналізатор коли-небудь запускався ...

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.