Panduan untuk menyelesaikan Metode di Java

1. Ikhtisar

Dalam tutorial ini, kita akan fokus pada aspek inti dari bahasa Java - metode penyelesaian yang disediakan oleh kelas Object root .

Sederhananya, ini dipanggil sebelum pengumpulan sampah untuk objek tertentu.

2. Menggunakan Finalizers

Metode finalize () disebut finalizer.

Finalizer dipanggil saat JVM mengetahui bahwa instance khusus ini harus dikumpulkan sampahnya. Finalizer semacam itu dapat melakukan operasi apa pun, termasuk menghidupkan kembali objek.

Namun, tujuan utama finalizer adalah untuk melepaskan sumber daya yang digunakan oleh objek sebelum dihapus dari memori. Finalizer dapat berfungsi sebagai mekanisme utama untuk operasi pembersihan, atau sebagai jaring pengaman ketika metode lain gagal.

Untuk memahami cara kerja finalis, mari kita lihat deklarasi 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 memiliki pembaca lapangan , yang mereferensikan sumber daya yang dapat ditutup. Ketika sebuah objek dibuat dari kelas ini, itu membangun sebuah contoh BufferedReader baru membaca dari sebuah file di classpath.

Contoh seperti itu digunakan dalam metode readFirstLine untuk mengekstrak baris pertama dalam file yang diberikan. Perhatikan bahwa pembaca tidak tertutup dalam kode yang diberikan.

Kita bisa melakukannya dengan menggunakan finalizer:

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

Sangat mudah untuk melihat bahwa finalizer dideklarasikan seperti metode instance normal lainnya.

Pada kenyataannya, waktu saat pengumpul sampah memanggil finalizer bergantung pada implementasi JVM dan kondisi sistem, yang berada di luar kendali kami.

Untuk membuat pengumpulan sampah terjadi di tempat, kami akan memanfaatkan metode System.gc . Dalam sistem dunia nyata, kita tidak boleh memanggilnya secara eksplisit, karena sejumlah alasan:

  1. Itu mahal
  2. Itu tidak langsung memicu pengumpulan sampah - ini hanya petunjuk bagi JVM untuk memulai GC
  3. JVM lebih tahu kapan GC perlu dipanggil

Jika kita perlu memaksa GC, kita bisa menggunakan jconsole untuk itu.

Berikut ini adalah kasus uji yang mendemonstrasikan pengoperasian finalizer:

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

Dalam pernyataan pertama, objek Finalizable dibuat, kemudian metode readFirstLine -nya dipanggil. Objek ini tidak ditugaskan ke variabel apa pun, oleh karena itu memenuhi syarat untuk pengumpulan sampah ketika metode System.gc dipanggil.

Penegasan dalam pengujian memverifikasi konten file input dan digunakan hanya untuk membuktikan bahwa kelas kustom kami berfungsi seperti yang diharapkan.

Saat kita menjalankan tes yang disediakan, sebuah pesan akan dicetak di konsol tentang buffered reader yang ditutup di finalizer. Ini menyiratkan bahwa metode finalisasi dipanggil dan telah membersihkan sumber daya.

Hingga saat ini, finalizer terlihat seperti cara yang bagus untuk operasi pra-penghancuran. Namun, itu tidak sepenuhnya benar.

Di bagian selanjutnya, kita akan melihat mengapa menggunakannya harus dihindari.

3. Menghindari Finalizers

Terlepas dari manfaat yang mereka bawa, finalis memiliki banyak kekurangan.

3.1. Kekurangan Finalizers

Mari kita lihat beberapa masalah yang akan kita hadapi saat menggunakan finalizer untuk melakukan tindakan kritis.

Masalah pertama yang terlihat adalah kurangnya ketepatan waktu. Kami tidak dapat mengetahui kapan finalizer berjalan karena pengumpulan sampah dapat terjadi kapan saja.

Dengan sendirinya, ini bukan masalah karena finalizer masih dijalankan, cepat atau lambat. Namun, sumber daya sistem tidak terbatas. Dengan demikian, kami mungkin kehabisan sumber daya sebelum pembersihan terjadi, yang dapat mengakibatkan sistem crash.

Finalisator juga berdampak pada portabilitas program. Karena algoritme pengumpulan sampah bergantung pada implementasi JVM, sebuah program dapat berjalan dengan sangat baik di satu sistem namun berperilaku berbeda di sistem lain.

Biaya kinerja adalah masalah signifikan lainnya yang datang dengan finalizer. Secara khusus, JVM harus melakukan lebih banyak operasi saat membuat dan menghancurkan objek yang berisi finalizer yang tidak kosong .

Masalah terakhir yang akan kita bicarakan adalah kurangnya penanganan eksepsi selama finalisasi. Jika finalizer melontarkan pengecualian, proses finalisasi berhenti, membiarkan objek dalam keadaan rusak tanpa pemberitahuan apa pun.

3.2. Demonstrasi Efek Finalisator

Saatnya mengesampingkan teori dan melihat efek finalisator dalam praktik.

Mari kita definisikan kelas baru dengan finalizer 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 metode finalize () - metode ini hanya mencetak string kosong ke konsol. Jika metode ini benar-benar kosong, JVM akan memperlakukan objek seolah-olah tidak memiliki finalizer. Oleh karena itu, kita perlu menyediakan finalize () dengan implementasi, yang hampir tidak melakukan apa-apa dalam kasus ini.

Di dalam metode utama , instance CrashedFinalizable baru dibuat di setiap iterasi loop for . Instance ini tidak ditetapkan ke variabel apa pun, sehingga memenuhi syarat untuk pengumpulan sampah.

Mari tambahkan beberapa pernyataan pada baris yang ditandai dengan // kode lain untuk melihat berapa banyak objek yang ada di memori saat runtime:

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 fokus pada konsep inti di Java - metode finalisasi . Ini terlihat berguna di atas kertas tetapi dapat memiliki efek samping yang buruk pada saat runtime. Dan yang lebih penting, selalu ada solusi alternatif untuk menggunakan finalizer.

Satu hal penting yang perlu diperhatikan adalah bahwa finalisasi sudah tidak digunakan lagi dimulai dengan Java 9 - dan pada akhirnya akan dihapus.

Seperti biasa, kode sumber untuk tutorial ini dapat ditemukan di GitHub.