1. Общ преглед
В този урок ще се съсредоточим върху основен аспект на езика Java - методът за финализиране , предоставен от основния клас Object .
Най-просто казано, това се извиква преди събирането на боклука за определен обект.
2. Използване на финализатори
Методът finalize () се нарича финализатор.
Финализаторите се извикват, когато JVM установи, че този конкретен екземпляр трябва да бъде събран боклук. Такъв финализатор може да извършва всякакви операции, включително да върне обекта към живот.
Основната цел на финализатора обаче е да освободи ресурси, използвани от обектите, преди да бъдат премахнати от паметта. Финализаторът може да работи като основен механизъм за почистващи операции или като предпазна мрежа, когато други методи се провалят.
За да разберем как работи финализаторът, нека разгледаме декларация за клас:
public class Finalizable { private BufferedReader reader; public Finalizable() { InputStream input = this.getClass() .getClassLoader() .getResourceAsStream("file.txt"); this.reader = new BufferedReader(new InputStreamReader(input)); } public String readFirstLine() throws IOException { String firstLine = reader.readLine(); return firstLine; } // other class members }
Класът Finalizable има полеви четец , който се позовава на близък ресурс. Когато даден обект се създава от този клас, той конструира нов BufferedReader например четене от файл в CLASSPATH.
Такъв екземпляр се използва в метода readFirstLine за извличане на първия ред в дадения файл. Забележете, че четецът не е затворен в дадения код.
Можем да направим това с помощта на финализатор:
@Override public void finalize() { try { reader.close(); System.out.println("Closed BufferedReader in the finalizer"); } catch (IOException e) { // ... } }
Лесно е да се види, че финализаторът се декларира точно както всеки нормален метод на инстанция.
В действителност времето, в което събирачът на боклук извиква финализатори, зависи от изпълнението на JVM и условията на системата, които са извън нашия контрол.
За да направим събирането на боклука на място, ще се възползваме от метода System.gc . В реални системи никога не трябва да се позоваваме изрично на това поради редица причини:
- Това е скъпо
- Това не задейства събирането на боклука веднага - това е само намек за JVM да стартира GC
- JVM знае по-добре кога трябва да се извика GC
Ако трябва да принудим GC, можем да използваме jconsole за това.
По-долу е тестов пример, демонстриращ работата на финализатор:
@Test public void whenGC_thenFinalizerExecuted() throws IOException { String firstLine = new Finalizable().readFirstLine(); assertEquals("baeldung.com", firstLine); System.gc(); }
През първото твърдение, а Finalizable обект е създаден, а след това му readFirstLine метод се нарича. Този обект не е присвоен на която и да е променлива, поради което отговаря на условията за събиране на боклука, когато бъде извикан методът System.gc .
Твърдението в теста проверява съдържанието на входния файл и се използва само за да докаже, че нашият потребителски клас работи както се очаква.
Когато стартираме предоставения тест, на конзолата ще се отпечата съобщение за затваряне на буферирания четец във финализатора. Това означава, че е бил извикан методът за финализиране и той е изчистил ресурса.
До този момент финализаторите изглеждат като чудесен начин за операции за предварително унищожаване. Това обаче не е съвсем вярно.
В следващия раздел ще видим защо тяхното използване трябва да се избягва.
3. Избягване на финализатори
Въпреки ползите, които носят, финализаторите идват с много недостатъци.
3.1. Недостатъци на финализаторите
Нека да разгледаме няколко проблема, с които ще се сблъскаме, когато използваме финализатори за извършване на критични действия.
Първият забележим проблем е липсата на бързина. Не можем да знаем кога се изпълнява финализатор, тъй като събирането на боклука може да се случи по всяко време.
Само по себе си това не е проблем, защото финализаторът все още се изпълнява, рано или късно. Системните ресурси обаче не са неограничени. По този начин може да останем без ресурси, преди да се извърши почистване, което може да доведе до срив на системата.
Финализаторите също оказват влияние върху преносимостта на програмата. Тъй като алгоритъмът за събиране на боклука зависи от изпълнението на JVM, една програма може да работи много добре в една система, докато се държи по различен начин в друга.
Разходът за производителност е друг важен проблем, който идва с финализаторите. По-конкретно, JVM трябва да изпълнява много повече операции, когато конструира и унищожава обекти, съдържащи непразен финализатор .
Последният проблем, за който ще говорим, е липсата на обработка на изключения по време на финализирането. Ако финализаторът изхвърли изключение, процесът на финализиране спира, оставяйки обекта в повредено състояние без никакво известие.
3.2. Демонстрация на ефектите на финализаторите
Време е да оставим теорията настрана и да видим ефектите от финализаторите на практика.
Нека дефинираме нов клас с непразен финализатор:
public class CrashedFinalizable { public static void main(String[] args) throws ReflectiveOperationException { for (int i = 0; ; i++) { new CrashedFinalizable(); // other code } } @Override protected void finalize() { System.out.print(""); } }
Забележете метода finalize () - той просто отпечатва празен низ в конзолата. Ако този метод беше напълно празен, JVM щеше да третира обекта така, сякаш нямаше финализатор. Следователно трябва да осигурим finalize () с изпълнение, което в този случай не прави почти нищо.
Вътре в основния метод се създава нов екземпляр CrashedFinalizable във всяка итерация на цикъла for . Този екземпляр не е присвоен на която и да е променлива, следователно отговаря на условията за събиране на боклука.
Нека добавим няколко израза на реда, маркиран с // друг код, за да видим колко обекта съществуват в паметта по време на изпълнение:
if ((i % 1_000_000) == 0) { Class finalizerClass = Class.forName("java.lang.ref.Finalizer"); Field queueStaticField = finalizerClass.getDeclaredField("queue"); queueStaticField.setAccessible(true); ReferenceQueue referenceQueue = (ReferenceQueue) queueStaticField.get(null); Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength"); queueLengthField.setAccessible(true); long queueLength = (long) queueLengthField.get(referenceQueue); System.out.format("There are %d references in the queue%n", queueLength); }
The given statements access some fields in internal JVM classes and print out the number of object references after every million iterations.
Let's start the program by executing the main method. We may expect it to run indefinitely, but that's not the case. After a few minutes, we should see the system crash with an error similar to this:
... There are 21914844 references in the queue There are 22858923 references in the queue There are 24202629 references in the queue There are 24621725 references in the queue There are 25410983 references in the queue There are 26231621 references in the queue There are 26975913 references in the queue Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at java.lang.ref.Finalizer.register(Finalizer.java:91) at java.lang.Object.(Object.java:37) at com.baeldung.finalize.CrashedFinalizable.(CrashedFinalizable.java:6) at com.baeldung.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9) Process finished with exit code 1
Looks like the garbage collector didn't do its job well – the number of objects kept increasing until the system crashed.
If we removed the finalizer, the number of references would usually be 0 and the program would keep running forever.
3.3. Explanation
To understand why the garbage collector didn't discard objects as it should, we need to look at how the JVM works internally.
When creating an object, also called a referent, that has a finalizer, the JVM creates an accompanying reference object of type java.lang.ref.Finalizer. After the referent is ready for garbage collection, the JVM marks the reference object as ready for processing and puts it into a reference queue.
We can access this queue via the static field queue in the java.lang.ref.Finalizer class.
Meanwhile, a special daemon thread called Finalizer keeps running and looks for objects in the reference queue. When it finds one, it removes the reference object from the queue and calls the finalizer on the referent.
During the next garbage collection cycle, the referent will be discarded – when it's no longer referenced from a reference object.
If a thread keeps producing objects at a high speed, which is what happened in our example, the Finalizer thread cannot keep up. Eventually, the memory won't be able to store all the objects, and we end up with an OutOfMemoryError.
Notice a situation where objects are created at warp speed as shown in this section doesn't often happen in real life. However, it demonstrates an important point – finalizers are very expensive.
4. No-Finalizer Example
Let's explore a solution providing the same functionality but without the use of finalize() method. Notice that the example below isn't the only way to replace finalizers.
Instead, it's used to demonstrate an important point: there are always options that help us to avoid finalizers.
Here's the declaration of our new class:
public class CloseableResource implements AutoCloseable { private BufferedReader reader; public CloseableResource() { InputStream input = this.getClass() .getClassLoader() .getResourceAsStream("file.txt"); reader = new BufferedReader(new InputStreamReader(input)); } public String readFirstLine() throws IOException { String firstLine = reader.readLine(); return firstLine; } @Override public void close() { try { reader.close(); System.out.println("Closed BufferedReader in the close method"); } catch (IOException e) { // handle exception } } }
It's not hard to see that the only difference between the new CloseableResource class and our previous Finalizable class is the implementation of the AutoCloseable interface instead of a finalizer definition.
Notice that the body of the close method of CloseableResource is almost the same as the body of the finalizer in class Finalizable.
The following is a test method, which reads an input file and releases the resource after finishing its job:
@Test public void whenTryWResourcesExits_thenResourceClosed() throws IOException { try (CloseableResource resource = new CloseableResource()) { String firstLine = resource.readFirstLine(); assertEquals("baeldung.com", firstLine); } }
In the above test, a CloseableResource instance is created in the try block of a try-with-resources statement, hence that resource is automatically closed when the try-with-resources block completes execution.
Running the given test method, we'll see a message printed out from the close method of the CloseableResource class.
5. Conclusion
В този урок се фокусирахме върху основна концепция в Java - методът за финализиране . Това изглежда полезно на хартия, но може да има грозни странични ефекти по време на работа. И по-важното е, че винаги има алтернативно решение за използване на финализатор.
Един критичен момент, който трябва да забележите, е, че финализирането е оттеглено, започвайки с Java 9 - и в крайна сметка ще бъде премахнато.
Както винаги, изходният код за този урок може да бъде намерен в GitHub.