Опасностите на метода за финализиране

И тук можете да получите грант за тестов период на Yandex.Cloud. Необходимо е само да въведете "Habr" в полето "секретна парола".

Чете сега

Безплатен YouTube поток на Joker 2017: Java 9, Concurrency, GC, Spring и разбира се пъзели

Как се отървахме от паузите на GC с нашето собствено решение за съхранение извън купчината на Java

Как да се справим с паузите на java приложение, без да докосваме GC

Коментари 19

Като цяло, добро и полезно напомняне.

Само два коментара:

По-добре е да освободите ресурси програмно и да влезете в метода за финализиране, ако по някаква причина това не е направено, за да намерите и отстраните проблема навреме.

Ако използваме собствен логер, има шанс той вече да е финализиран в момента, в който нашият клас е финализиран. Например, това може да е случаят, когато използвате System.runFinalizersOnExit(), ако последният ред на програмата затваря регистратора.

7. Всички изключения, хвърлени в тялото на метода, ще бъдат игнорирани.

Всяко изключение, хвърлено от метода finalize, води до спиране на финализирането на този обект

Наистина в спецификацията не пише, че няма да има сглобяване, но на практика паметта не се освобождава и рано или късно OOM ни чака. Случи ми се в един проект и най-вече ми направи впечатление, че изключението беше мълчаливо игнорирано. Разбира се, може би всичко зависи от времето за изпълнение и същият OpenJDK не страда от това.

Ето един експеримент на един ентусиаст: elliottback.com/wp/java-memory-leaks-w-finalize-examples/ Мисля, че можете да намерите други, ако желаете.

Ако вашият регистратор има метод за финализиране, тогава трябва да използвате защитна техника: регистраторът трябва да има непостоянен флаг, който се задава, когато се финализира, и всички методи за регистриране първо проверяват товазнаме.

Благодаря и за второто.

Честно казано, не мисля, че е много по-бързо. Мисля, че въпросът тук е друг. Нека оставим зад кулисите, че finalize() има тромава семантика до степен на невъзможност - ще говорим само за производителност: ако използвате finalize(), тогава паметта под самия ви обект (който е в Java heap) няма да бъде освободена, докато финализирането не проработи. Освен това паметта под всички обекти, достъпни от вашия обект, няма да бъде освободена (те трябва да останат достъпни, докато завърши финализирането). Това създава много неочевидни проблеми: например трудно ми е да си представя как можете да приложите събиране на отпадъци с финализиране за обекти в младото поколение, които се събират от копиращия GC. Почти сигурно всички обекти с finalize() ще бъдат прехвърлени към old-gen и fullGC ще бъдат събрани (изглежда дори това беше изрично описано някъде, но не помня точно).

В същото време, използвайки фантомни препратки, времето за изпълнение няма да има проблеми с освобождаването на памет в Java heap - паметта под самия обект и всички обекти, достъпни само от него, могат да бъдат освободени незабавно, веднага щом самият обект стане недостъпен. Включително веднага в по-младото поколение, бързо копиращ колекционер. В края на краищата Runnable в Cleaner съхранява (== трябва да съхранява - в идеалния случай) връзки само към _външни_ ресурси, към ресурси извън купчината. Те - и само те - ще изчакат, докато се стигне до обработка на съдържанието на ReferenceQueue. Тоест този механизъм наистина е много по-евтин по отношение на натоварването на системата за управление на паметта в самата Java.

Е, това са мои предположения. Самият обект ще бъде освободен, но Runnable, който създадохме за освобождаване на външни ресурси, няма. Той ще чака, докато го изтеглят от опашката. Така че балансът тук е труден.Оказва се, между размера на този Runnable и размера на оригиналния обект + всички достъпни само от него.

Във всеки случай finalize() има толкова сложна семантика, че внедряването му трябва да е скъпо именно поради принудителното изпълнение. По-прозрачната логика на ReferenceQueue вероятно ще бъде внедрена по-лесно и по-ефективно.