Finalisasi dan pembersihan objek

Tiga bulan lalu, saya memulai artikel seri mini tentang mendesain objek dengan diskusi tentang prinsip desain yang berfokus pada inisialisasi yang tepat di awal kehidupan sebuah objek. Dalam artikel Teknik Desain ini, saya akan berfokus pada prinsip desain yang membantu Anda memastikan pembersihan yang tepat di akhir masa pakai suatu objek.

Mengapa membersihkan?

Setiap objek dalam program Java menggunakan sumber daya komputasi yang terbatas. Yang paling jelas, semua objek menggunakan beberapa memori untuk menyimpan gambar mereka di heap. (Ini berlaku bahkan untuk objek yang tidak mendeklarasikan variabel instan. Setiap gambar objek harus menyertakan beberapa jenis pointer ke data kelas, dan dapat menyertakan informasi lain yang bergantung pada implementasi.) Tetapi objek juga dapat menggunakan sumber daya terbatas selain memori. Misalnya, beberapa objek mungkin menggunakan sumber daya seperti pegangan file, konteks grafik, soket, dan sebagainya. Saat Anda mendesain sebuah objek, Anda harus memastikannya pada akhirnya melepaskan sumber daya terbatas apa pun yang digunakannya sehingga sistem tidak akan kehabisan sumber daya tersebut.

Karena Java adalah bahasa yang dikumpulkan sampah, melepaskan memori yang terkait dengan objek itu mudah. Yang perlu Anda lakukan adalah melepaskan semua referensi ke objek tersebut. Karena Anda tidak perlu khawatir tentang membebaskan objek secara eksplisit, seperti yang harus dilakukan dalam bahasa seperti C atau C ++, Anda tidak perlu khawatir akan merusak memori dengan secara tidak sengaja membebaskan objek yang sama dua kali. Namun, Anda perlu memastikan bahwa Anda benar-benar melepaskan semua referensi ke objek tersebut. Jika tidak, Anda bisa berakhir dengan kebocoran memori, seperti kebocoran memori yang Anda dapatkan dalam program C ++ saat Anda lupa untuk membebaskan objek secara eksplisit. Namun demikian, selama Anda melepaskan semua referensi ke suatu objek, Anda tidak perlu khawatir tentang "membebaskan" memori tersebut secara eksplisit.

Demikian pula, Anda tidak perlu khawatir tentang secara eksplisit membebaskan objek konstituen yang direferensikan oleh variabel instance dari objek yang tidak lagi Anda perlukan. Melepaskan semua referensi ke objek yang tidak dibutuhkan akan membuat referensi objek konstituen apa pun yang terkandung dalam variabel instance objek tersebut tidak berlaku lagi. Jika referensi yang sekarang tidak valid adalah satu-satunya referensi yang tersisa ke objek konstituen tersebut, objek konstituen juga akan tersedia untuk pengumpulan sampah. Sepotong kue, bukan?

Aturan pengumpulan sampah

Meskipun pengumpulan sampah memang membuat manajemen memori di Java jauh lebih mudah daripada di C atau C ++, Anda tidak dapat sepenuhnya melupakan memori saat Anda memprogram di Java. Untuk mengetahui kapan Anda mungkin perlu memikirkan tentang manajemen memori di Java, Anda perlu tahu sedikit tentang cara penanganan pengumpulan sampah dalam spesifikasi Java.

Pengumpulan sampah tidak diwajibkan

Hal pertama yang perlu diketahui adalah bahwa tidak peduli seberapa rajin Anda mencari melalui Spesifikasi Mesin Virtual Java (JVM Spec), Anda tidak akan dapat menemukan kalimat yang memerintahkan, Setiap JVM harus memiliki pengumpul sampah. Spesifikasi Mesin Virtual Java memberi desainer VM banyak kelonggaran dalam memutuskan bagaimana implementasi mereka akan mengelola memori, termasuk memutuskan apakah akan menggunakan pengumpulan sampah sama sekali atau tidak. Jadi, ada kemungkinan bahwa beberapa JVM (seperti JVM kartu pintar tanpa tulang) mungkin mengharuskan program yang dijalankan di setiap sesi "sesuai" dalam memori yang tersedia.

Tentu saja, Anda selalu dapat kehabisan memori, bahkan pada sistem memori virtual. Spesifikasi JVM tidak menyatakan berapa banyak memori yang akan tersedia untuk JVM. Itu hanya menyatakan bahwa setiap kali JVM tidak kehabisan memori, itu harus membuang OutOfMemoryError.

Namun demikian, untuk memberikan aplikasi Java kesempatan terbaik untuk dijalankan tanpa kehabisan memori, kebanyakan JVM akan menggunakan pengumpul sampah. Pengumpul sampah mengambil kembali memori yang ditempati oleh objek yang tidak direferensikan di heap, sehingga memori dapat digunakan lagi oleh objek baru, dan biasanya menghapus fragmen heap saat program berjalan.

Algoritme pengumpulan sampah tidak ditentukan

Perintah lain yang tidak akan Anda temukan dalam spesifikasi JVM adalah Semua JVM yang menggunakan pengumpulan sampah harus menggunakan algoritme XXX. Perancang setiap JVM dapat memutuskan bagaimana pengumpulan sampah akan bekerja dalam implementasinya. Algoritma pengumpulan sampah adalah salah satu area di mana vendor JVM dapat berusaha untuk membuat implementasinya lebih baik daripada pesaing. Ini penting bagi Anda sebagai programmer Java karena alasan berikut:

Karena secara umum Anda tidak tahu bagaimana pengumpulan sampah akan dilakukan di dalam JVM, Anda tidak tahu kapan objek tertentu akan dikumpulkan sampahnya.

Terus? Anda mungkin bertanya. Alasan Anda mungkin peduli saat suatu objek dikumpulkan sampah ada hubungannya dengan finalizer. ( Finalizer didefinisikan sebagai metode instance Java biasa bernama finalize()yang mengembalikan void dan tidak menggunakan argumen.) Spesifikasi Java membuat janji berikut tentang finalizer:

Sebelum mengambil kembali memori yang ditempati oleh objek yang memiliki finalizer, pengumpul sampah akan memanggil finalizer objek tersebut.

Mengingat bahwa Anda tidak tahu kapan objek akan dikumpulkan sampah, tetapi Anda tahu bahwa objek yang dapat diselesaikan akan diselesaikan saat sampah dikumpulkan, Anda dapat membuat pengurangan besar berikut:

Anda tidak tahu kapan objek akan diselesaikan.

Anda harus menanamkan fakta penting ini di otak Anda dan selamanya membiarkannya menginformasikan desain objek Java Anda.

Finalisator yang harus dihindari

Prinsip utama tentang finalisator adalah ini:

Jangan merancang program Java Anda sedemikian rupa sehingga kebenarannya bergantung pada penyelesaian yang "tepat waktu".

Dengan kata lain, jangan menulis program yang akan rusak jika objek tertentu tidak diselesaikan oleh titik-titik tertentu selama masa eksekusi program. Jika Anda menulis program seperti itu, program itu mungkin bekerja pada beberapa implementasi JVM tetapi gagal pada yang lain.

Jangan mengandalkan finalizer untuk melepaskan sumber daya non-memori

Contoh objek yang melanggar aturan ini adalah yang membuka file dalam konstruktornya dan menutup file dalam finalize()metodenya. Meskipun desain ini tampak rapi, rapi, dan simetris, namun berpotensi menimbulkan bug yang membahayakan. Sebuah program Java umumnya hanya akan memiliki sejumlah file menangani yang terbatas. Ketika semua pegangan itu digunakan, program tidak akan dapat membuka file lagi.

Program Java yang menggunakan objek seperti itu (yang membuka file di konstruktornya dan menutupnya di finalizer) mungkin berfungsi dengan baik pada beberapa implementasi JVM. Pada implementasi seperti itu, finalisasi akan terjadi cukup sering untuk menjaga ketersediaan file handle dalam jumlah yang cukup setiap saat. Tetapi program yang sama mungkin gagal pada JVM berbeda yang pengumpul sampahnya tidak cukup sering diselesaikan untuk menjaga program agar tidak kehabisan pegangan file. Atau, yang lebih berbahaya lagi, program ini mungkin bekerja pada semua implementasi JVM sekarang tetapi gagal dalam situasi misi kritis beberapa tahun (dan siklus rilis) di masa mendatang.

Aturan praktis finalizer lainnya

Dua keputusan lain yang diserahkan kepada desainer JVM adalah memilih utas (atau utas) yang akan mengeksekusi finalisator dan urutan finalisator akan dijalankan. Finalizer dapat dijalankan dalam urutan apa pun - secara berurutan dengan satu thread atau secara bersamaan dengan beberapa thread. Jika program Anda entah bagaimana bergantung pada ketepatan pada finalizer yang dijalankan dalam urutan tertentu, atau oleh utas tertentu, itu mungkin bekerja pada beberapa implementasi JVM tetapi gagal pada yang lain.

Anda juga harus ingat bahwa Java menganggap objek akan diselesaikan apakah finalize()metode tersebut kembali secara normal atau selesai secara tiba-tiba dengan melontarkan pengecualian. Pengumpul sampah mengabaikan pengecualian apa pun yang diberikan oleh finalisator dan sama sekali tidak memberi tahu aplikasi lainnya bahwa pengecualian telah dilemparkan. Jika Anda perlu memastikan bahwa finalizer tertentu menyelesaikan misi tertentu sepenuhnya, Anda harus menulis finalizer tersebut sehingga menangani pengecualian apa pun yang mungkin muncul sebelum finalizer menyelesaikan misinya.

Satu lagi aturan praktis tentang finalizer berkaitan dengan objek yang tertinggal di heap pada akhir masa pakai aplikasi. Secara default, pengumpul sampah tidak akan mengeksekusi finalisator objek apa pun yang tersisa di heap saat aplikasi ditutup. Untuk mengubah default ini, Anda harus menjalankan runFinalizersOnExit()metode kelas Runtimeatau System, meneruskan truesebagai parameter tunggal. Jika program Anda berisi objek yang finalisasinya harus benar-benar dipanggil sebelum program keluar, pastikan untuk memanggil runFinalizersOnExit()di suatu tempat dalam program Anda.

Jadi, apa kegunaan finalizer?

Sekarang Anda mungkin merasa bahwa Anda tidak memiliki banyak kegunaan untuk finalisator. Meskipun sebagian besar kelas yang Anda desain tidak akan menyertakan finalizer, ada beberapa alasan untuk menggunakan finalizer.

Satu aplikasi yang masuk akal, meskipun jarang, untuk finalizer adalah untuk membebaskan memori yang dialokasikan oleh metode asli. Jika sebuah objek memanggil metode native yang mengalokasikan memori (mungkin fungsi C yang memanggil malloc()), finalizer objek tersebut dapat memanggil metode native yang membebaskan memori (panggilan free()) tersebut. Dalam situasi ini, Anda akan menggunakan finalizer untuk mengosongkan memori yang dialokasikan atas nama objek - memori yang tidak akan diambil kembali secara otomatis oleh pengumpul sampah.

Penggunaan finalizer lain yang lebih umum adalah menyediakan mekanisme fallback untuk melepaskan sumber daya terbatas non-memori seperti gagang atau soket file. Seperti disebutkan sebelumnya, Anda tidak boleh mengandalkan finalizer untuk merilis sumber daya non-memori yang terbatas. Sebagai gantinya, Anda harus menyediakan metode yang akan merilis sumber daya. Tetapi Anda mungkin juga ingin menyertakan finalizer yang memeriksa untuk memastikan sumber daya telah dirilis, dan jika belum, itu akan terus berlanjut dan merilisnya. Finalizer seperti itu melindungi dari (dan semoga tidak akan mendorong) penggunaan kelas Anda yang ceroboh. Jika pemrogram klien lupa menjalankan metode yang Anda sediakan untuk melepaskan sumber daya, finalizer akan melepaskan sumber daya jika objek pernah sampah dikumpulkan. The finalize()metode yangLogFileManager kelas, yang ditampilkan nanti di artikel ini, adalah contoh dari jenis finalizer ini.

Hindari penyalahgunaan finalizer

Adanya finalisasi menghasilkan beberapa komplikasi menarik untuk JVM dan beberapa kemungkinan menarik bagi programmer Java. Apa yang diberikan finalisasi untuk programmer adalah kekuatan atas hidup dan mati objek. Singkatnya, adalah mungkin dan sepenuhnya legal di Java untuk menghidupkan kembali objek di finalizer - menghidupkannya kembali dengan membuatnya direferensikan lagi. (Salah satu cara finalisator dapat melakukannya adalah dengan menambahkan referensi ke objek yang sedang diselesaikan ke daftar tertaut statis yang masih "aktif".) Meskipun kekuatan seperti itu mungkin menggoda untuk dilakukan karena membuat Anda merasa penting, aturan praktisnya adalah menahan godaan untuk menggunakan kekuatan ini. Secara umum, menghidupkan kembali objek di finalizer merupakan penyalahgunaan finalizer.

Alasan utama untuk aturan ini adalah bahwa program apa pun yang menggunakan kebangkitan dapat didesain ulang menjadi program yang lebih mudah dipahami yang tidak menggunakan kebangkitan. Sebuah bukti formal dari teorema ini dibiarkan sebagai latihan untuk pembaca (saya selalu ingin mengatakan itu), tetapi dalam semangat informal, anggap bahwa kebangkitan objek akan seacak dan tidak dapat diprediksi seperti finalisasi objek. Dengan demikian, desain yang menggunakan kebangkitan akan sulit diketahui oleh pemrogram pemeliharaan berikutnya yang terjadi bersama - yang mungkin tidak sepenuhnya memahami keistimewaan pengumpulan sampah di Java.

Jika Anda merasa Anda harus menghidupkan kembali sebuah objek, pertimbangkan untuk mengkloning salinan baru dari objek tersebut alih-alih menghidupkan kembali objek lama yang sama. Alasan di balik saran ini adalah bahwa pengumpul sampah di JVM memanggil finalize()metode objek hanya sekali. Jika objek itu dibangkitkan dan tersedia untuk pengumpulan sampah untuk kedua kalinya, metode objek finalize()tidak akan dipanggil lagi.