Panduan untuk menyelesaikan kaedah di Jawa

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan memfokuskan pada aspek inti dari bahasa Java - kaedah akhir yang disediakan oleh kelas Object Object .

Secara sederhana, ini dipanggil sebelum pengumpulan sampah untuk objek tertentu.

2. Menggunakan Finalizer

Kaedah finalize () dipanggil finalizer.

Penyelesai diminta apabila JVM mengetahui bahawa contoh ini harus dikumpulkan sampah. Penyelesai seperti itu boleh melakukan sebarang operasi, termasuk menghidupkan semula objek tersebut.

Walau bagaimanapun, tujuan utama penentu adalah untuk melepaskan sumber yang digunakan oleh objek sebelum ia dikeluarkan dari ingatan. Penyelesaikan boleh berfungsi sebagai mekanisme utama operasi pembersihan, atau sebagai jaring pengaman apabila kaedah lain gagal.

Untuk memahami bagaimana penyusun kerja berfungsi, mari kita lihat perisytiharan kelas:

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 }

Kelas Finalizable mempunyai pembaca lapangan , yang merujuk sumber yang boleh ditutup. Apabila objek dibuat dari kelas ini, objek tersebut akan membina bacaan BufferedReader baru dari fail di classpath.

Contoh seperti itu digunakan dalam kaedah readFirstLine untuk mengekstrak baris pertama dalam fail yang diberikan. Perhatikan bahawa pembaca tidak ditutup dalam kod yang diberikan.

Kita boleh melakukannya dengan menggunakan pemudah cara:

@Override public void finalize() { try { reader.close(); System.out.println("Closed BufferedReader in the finalizer"); } catch (IOException e) { // ... } }

Sangat mudah untuk melihat bahawa penentu dinyatakan seperti kaedah contoh biasa.

Pada hakikatnya, waktu di mana pemungut sampah memanggil finalis bergantung pada pelaksanaan JVM dan keadaan sistem, yang berada di luar kawalan kita.

Untuk membuat pengumpulan sampah dilakukan di tempat, kami akan memanfaatkan kaedah System.gc . Dalam sistem dunia nyata, kita tidak boleh menggunakannya secara eksplisit, kerana beberapa sebab:

  1. Ia mahal
  2. Ia tidak mencetuskan pengumpulan sampah dengan segera - ini hanya petunjuk JVM untuk memulakan GC
  3. JVM lebih tahu bila GC perlu dipanggil

Sekiranya kita perlu memaksa GC, kita boleh menggunakan jconsole untuk itu.

Berikut ini adalah kes ujian yang menunjukkan pengoperasian penentu:

@Test public void whenGC_thenFinalizerExecuted() throws IOException { String firstLine = new Finalizable().readFirstLine(); assertEquals("baeldung.com", firstLine); System.gc(); }

Dalam satu kenyataan yang pertama, yang Finalizable objek dicipta, kemudian yang readFirstLine kaedah dipanggil. Objek ini tidak diberikan kepada sebarang pemboleh ubah, oleh itu ia layak untuk pengumpulan sampah apabila kaedah System.gc dipanggil.

Penegasan dalam ujian mengesahkan kandungan fail input dan digunakan hanya untuk membuktikan bahawa kelas khusus kami berfungsi seperti yang diharapkan.

Apabila kita menjalankan ujian yang disediakan, satu mesej akan dicetak di konsol mengenai pembaca yang disekat ditutup di finalis. Ini membayangkan Finalize kaedah telah dipanggil dan ia telah dibersihkan sumber.

Hingga tahap ini, penyelesai kelihatan seperti kaedah yang bagus untuk operasi pra-pemusnahan. Namun, itu tidak benar.

Di bahagian seterusnya, kita akan melihat mengapa penggunaannya harus dielakkan.

3. Mengelakkan Penyelesaian

Di sebalik faedah yang mereka perolehi, para pemenang akan memperoleh banyak kekurangan.

3.1. Kekurangan Pemuktamadkan

Mari kita lihat beberapa masalah yang akan kita hadapi ketika menggunakan finalis untuk melakukan tindakan kritikal.

Isu pertama yang dapat dilihat adalah kekurangan segera. Kami tidak dapat mengetahui kapan penentu dijalankan kerana pengumpulan sampah mungkin berlaku pada bila-bila masa.

Dengan sendirinya, ini tidak menjadi masalah kerana penyelesai masih dijalankan, cepat atau lambat. Walau bagaimanapun, sumber sistem tidak terhad. Oleh itu, kita mungkin kehabisan sumber sebelum pembersihan berlaku, yang boleh mengakibatkan sistem rosak.

Penyelesai juga memberi kesan kepada kebolehmampuan program. Oleh kerana algoritma pengumpulan sampah bergantung pada pelaksanaan JVM, program mungkin berjalan dengan baik pada satu sistem sambil berkelakuan berbeza pada sistem yang lain.

Kos prestasi adalah satu lagi masalah penting yang disertakan dengan pemenang. Secara khusus, JVM mesti melakukan lebih banyak operasi semasa membina dan memusnahkan objek yang mengandungi pemadat yang tidak kosong .

Masalah terakhir yang akan kita bicarakan adalah kurangnya pengendalian pengecualian semasa finalisasi. Sekiranya finalis membuat pengecualian, proses finalisasi akan berhenti, membiarkan objek dalam keadaan rosak tanpa pemberitahuan.

3.2. Demonstrasi Kesan Penyelesaian

Sudah tiba masanya untuk mengetepikan teori dan melihat kesan pemenang dalam praktiknya.

Mari tentukan kelas baru dengan pemudahcara yang tidak kosong:

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(""); } }

Perhatikan kaedah finalize () - ia hanya mencetak rentetan kosong ke konsol. Sekiranya kaedah ini benar-benar kosong, JVM akan memperlakukan objek itu seolah-olah tidak mempunyai finalis. Oleh itu, kita perlu menyediakan penyelesaian () dengan pelaksanaan, yang hampir tidak ada dalam kes ini.

Di dalam kaedah utama , contoh CrashedFinalizable baru dibuat dalam setiap lelaran loop untuk . Contoh ini tidak diberikan kepada pemboleh ubah apa pun, oleh itu layak untuk pengumpulan sampah.

Mari tambahkan beberapa pernyataan pada baris yang ditandai dengan // kod lain untuk melihat berapa banyak objek yang ada di dalam memori semasa menjalankan:

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

Dalam tutorial ini, kami memfokuskan pada konsep teras di Java - kaedah menyelesaikan . Ini kelihatan berguna di atas kertas tetapi boleh memberi kesan sampingan yang buruk pada waktu berjalan. Dan yang lebih penting lagi, selalu ada penyelesaian alternatif untuk menggunakan finalis.

Satu titik penting untuk diperhatikan adalah bahawa finalisasi sudah tidak digunakan lagi dengan Java 9 - dan akhirnya akan dikeluarkan.

Seperti biasa, kod sumber untuk tutorial ini boleh didapati di GitHub.