1. ภาพรวม
ในบทช่วยสอนนี้เราจะเน้นที่ลักษณะหลักของภาษา Java ซึ่งเป็นวิธีการสรุปโดยคลาสอ็อบเจ็กต์รูท
พูดง่ายๆคือเรียกก่อนการรวบรวมขยะสำหรับวัตถุเฉพาะ
2. การใช้ Finalizers
วิธีการFinalize ()เรียกว่า finalizer
Finalizers จะถูกเรียกใช้เมื่อ JVM พบว่าอินสแตนซ์นี้ควรเป็นขยะ Finalizer ดังกล่าวอาจดำเนินการใด ๆ รวมถึงทำให้วัตถุกลับมามีชีวิตอีกครั้ง
อย่างไรก็ตามจุดประสงค์หลักของ Finalizer คือเพื่อปล่อยทรัพยากรที่ใช้โดยออบเจ็กต์ก่อนที่จะถูกลบออกจากหน่วยความจำ Finalizer สามารถทำงานเป็นกลไกหลักสำหรับการดำเนินการล้างข้อมูลหรือเป็นตาข่ายนิรภัยเมื่อวิธีการอื่นล้มเหลว
เพื่อทำความเข้าใจว่า Finalizer ทำงานอย่างไรมาดูการประกาศคลาส:
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 }
คลาสที่สรุปได้มีตัวอ่านฟิลด์ซึ่งอ้างอิงถึงทรัพยากรที่สามารถปิดได้ เมื่ออ็อบเจ็กต์ถูกสร้างจากคลาสนี้อ็อบเจ็กต์จะสร้างอินสแตนซ์BufferedReaderใหม่ที่อ่านจากไฟล์ในคลาสพา ธ
อินสแตนซ์ดังกล่าวถูกใช้ในเมธอดreadFirstLineเพื่อแยกบรรทัดแรกในไฟล์ที่กำหนด สังเกตว่าโปรแกรมอ่านไม่ได้ปิดอยู่ในรหัสที่กำหนด
เราสามารถทำได้โดยใช้ Finalizer:
@Override public void finalize() { try { reader.close(); System.out.println("Closed BufferedReader in the finalizer"); } catch (IOException e) { // ... } }
เป็นเรื่องง่ายที่จะเห็นว่ามีการประกาศ Finalizer เช่นเดียวกับวิธีการอินสแตนซ์ทั่วไป
ในความเป็นจริงเวลาที่ตัวเก็บรวบรวมขยะเรียกตัวสุดท้ายจะขึ้นอยู่กับการนำไปใช้งานของ JVM และเงื่อนไขของระบบซึ่งอยู่นอกเหนือการควบคุมของเรา
เพื่อให้การเก็บขยะเกิดขึ้นทันทีเราจะใช้ประโยชน์จากวิธีSystem.gc ในระบบโลกแห่งความเป็นจริงเราไม่ควรเรียกสิ่งนั้นอย่างชัดเจนด้วยเหตุผลหลายประการ:
- มันมีค่าใช้จ่ายสูง
- ไม่ได้ทริกเกอร์การรวบรวมขยะในทันที แต่เป็นเพียงคำใบ้ให้ JVM เริ่ม GC
- JVM รู้ดีกว่าเมื่อต้องเรียก GC
หากเราต้องการบังคับ GC เราสามารถใช้jconsoleได้
ต่อไปนี้เป็นกรณีทดสอบที่แสดงให้เห็นถึงการทำงานของ Finalizer:
@Test public void whenGC_thenFinalizerExecuted() throws IOException { String firstLine = new Finalizable().readFirstLine(); assertEquals("baeldung.com", firstLine); System.gc(); }
ในคำสั่งแรกอ็อบเจ็กต์ที่สรุปได้จะถูกสร้างขึ้นจากนั้นเมธอดreadFirstLineจะถูกเรียกใช้ วัตถุนี้ไม่ได้กำหนดให้กับตัวแปรใด ๆ ดังนั้นจึงมีสิทธิ์ได้รับการรวบรวมขยะเมื่อมีการเรียกใช้เมธอดSystem.gc
คำยืนยันในการทดสอบจะตรวจสอบเนื้อหาของไฟล์อินพุตและใช้เพื่อพิสูจน์ว่าคลาสที่กำหนดเองของเราทำงานได้ตามที่คาดไว้
เมื่อเราเรียกใช้การทดสอบที่ให้มาข้อความจะถูกพิมพ์บนคอนโซลเกี่ยวกับตัวอ่านบัฟเฟอร์ที่ถูกปิดในโปรแกรมสุดท้าย นี่หมายความว่ามีการเรียกวิธีการสุดท้ายและได้ล้างทรัพยากรแล้ว
เมื่อถึงจุดนี้ผู้เข้าชิงจึงดูเหมือนเป็นวิธีที่ยอดเยี่ยมสำหรับปฏิบัติการก่อนทำลายล้าง อย่างไรก็ตามนั่นไม่เป็นความจริงเลย
ในส่วนถัดไปเราจะดูว่าเหตุใดจึงควรหลีกเลี่ยงการใช้
3. การหลีกเลี่ยง Finalizers
แม้จะมีประโยชน์มากมาย แต่ผู้เข้ารอบสุดท้ายก็มีข้อเสียมากมาย
3.1. ข้อเสียของ Finalizers
มาดูปัญหาหลายอย่างที่เรากำลังเผชิญเมื่อใช้โปรแกรมสุดท้ายเพื่อดำเนินการที่สำคัญ
ปัญหาแรกที่เห็นได้ชัดเจนคือการขาดความรวดเร็ว เราไม่สามารถทราบได้ว่า Finalizer ทำงานเมื่อใดเนื่องจากการรวบรวมขยะอาจเกิดขึ้นได้ตลอดเวลา
ด้วยตัวมันเองนี่ไม่ใช่ปัญหาเพราะโปรแกรมสุดท้ายยังคงดำเนินการไม่ช้าก็เร็ว อย่างไรก็ตามทรัพยากรระบบไม่ จำกัด ดังนั้นเราอาจหมดทรัพยากรก่อนที่จะเกิดการล้างข้อมูลซึ่งอาจส่งผลให้ระบบขัดข้อง
Finalizers ยังมีผลต่อความสามารถในการพกพาของโปรแกรม เนื่องจากอัลกอริทึมการรวบรวมขยะขึ้นอยู่กับการใช้งาน JVM โปรแกรมอาจทำงานได้ดีมากบนระบบหนึ่งในขณะที่ทำงานแตกต่างกันไปในอีกระบบหนึ่ง
ต้นทุนด้านประสิทธิภาพเป็นอีกประเด็นสำคัญที่มาพร้อมกับตัวสรุป โดยเฉพาะJVM จะต้องดำเนินการอื่น ๆ อีกมากมายเมื่อสร้างและทำลายวัตถุที่มี finalizer
ปัญหาสุดท้ายที่เราจะพูดถึงคือการขาดการจัดการข้อยกเว้นในระหว่างการสรุป หากโปรแกรมปิดท้ายมีข้อยกเว้นกระบวนการสรุปจะหยุดลงและปล่อยให้วัตถุอยู่ในสถานะเสียหายโดยไม่มีการแจ้งเตือนใด ๆ
3.2. การสาธิตผลของ Finalizers
ถึงเวลาที่จะละทิ้งทฤษฎีและดูผลของ Finalizers ในทางปฏิบัติ
มากำหนดคลาสใหม่ด้วย Finalizer ที่ไม่ว่างเปล่า:
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 จะปฏิบัติต่อออบเจ็กต์ราวกับว่าไม่มีตัวสุดท้าย ดังนั้นเราจำเป็นต้องจัดเตรียมการใช้งานขั้นสุดท้าย ()ซึ่งแทบจะไม่ทำอะไรเลยในกรณีนี้
ภายในเมธอดหลักอินสแตนซ์CrashedFinalizableใหม่จะถูกสร้างขึ้นในการวนซ้ำของfor loop แต่ละครั้ง อินสแตนซ์นี้ไม่ได้กำหนดให้กับตัวแปรใด ๆ ดังนั้นจึงมีสิทธิ์ได้รับการรวบรวมขยะ
มาเพิ่มคำสั่งสองสามคำในบรรทัดที่มีเครื่องหมาย// อื่น ๆเพื่อดูว่ามีวัตถุอยู่ในหน่วยความจำที่รันไทม์:
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 นั่นคือวิธีการสรุป สิ่งนี้ดูมีประโยชน์บนกระดาษ แต่อาจมีผลข้างเคียงที่น่าเกลียดขณะรันไทม์ และที่สำคัญกว่านั้นคือมีทางเลือกอื่นในการใช้ Finalizer เสมอ
จุดสำคัญอย่างหนึ่งที่ต้องสังเกตคือการสรุปได้ถูกเลิกใช้งานโดยเริ่มจาก Java 9 และในที่สุดก็จะถูกลบออก
เช่นเคยซอร์สโค้ดสำหรับบทช่วยสอนนี้สามารถพบได้ใน GitHub