Penyelesaian dan pembersihan objek

Tiga bulan yang lalu, saya memulakan siri mini artikel mengenai merancang objek dengan perbincangan mengenai prinsip reka bentuk yang memfokuskan pada inisialisasi yang tepat pada awal kehidupan objek. Dalam artikel Teknik Reka Bentuk ini, saya akan memfokuskan pada prinsip reka bentuk yang membantu anda memastikan pembersihan yang betul pada akhir hayat objek.

Mengapa membersihkan?

Setiap objek dalam program Java menggunakan sumber pengkomputeran yang terbatas. Paling jelas, semua objek menggunakan sedikit memori untuk menyimpan gambarnya di timbunan. (Hal ini berlaku bahkan untuk objek yang tidak menyatakan pemboleh ubah contoh. Setiap gambar objek mesti menyertakan beberapa jenis penunjuk ke data kelas, dan dapat menyertakan informasi yang bergantung pada pelaksanaan juga.) Tetapi objek juga dapat menggunakan sumber terbatas lain selain memori. Sebagai contoh, beberapa objek mungkin menggunakan sumber seperti pengendalian fail, konteks grafik, soket, dan sebagainya. Semasa anda merancang objek, anda mesti memastikan ia akhirnya melepaskan sumber daya yang digunakan sehingga sistem tidak akan kehabisan sumber tersebut.

Kerana Java adalah bahasa yang dikumpulkan sampah, melepaskan memori yang berkaitan dengan objek itu mudah. Yang perlu anda lakukan ialah melepaskan semua rujukan ke objek tersebut. Kerana anda tidak perlu bimbang membebaskan objek secara eksplisit, kerana anda mesti menggunakan bahasa seperti C atau C ++, anda tidak perlu risau untuk merosakkan memori dengan membebaskan objek yang sama secara tidak sengaja dua kali. Namun, anda perlu memastikan bahawa anda benar-benar melepaskan semua rujukan ke objek tersebut. Sekiranya tidak, anda boleh berakhir dengan kebocoran memori, seperti kebocoran memori yang anda dapatkan dalam program C ++ apabila anda lupa untuk membebaskan objek secara jelas. Walaupun begitu, sepanjang anda melepaskan semua rujukan ke suatu objek, anda tidak perlu bimbang tentang "membebaskan" memori itu secara terang-terangan.

Begitu juga, anda tidak perlu risau untuk membebaskan secara eksplisit objek konstituen yang dirujuk oleh pemboleh ubah contoh objek yang tidak lagi anda perlukan. Melepaskan semua rujukan ke objek yang tidak diperlukan akan membatalkan rujukan objek konstituen yang terdapat dalam pemboleh ubah contoh objek tersebut. Sekiranya rujukan yang tidak sah sekarang adalah satu-satunya rujukan yang tersisa untuk objek penyusun itu, objek penyusun juga akan tersedia untuk pengumpulan sampah. Sekeping kek, bukan?

Peraturan pengumpulan sampah

Walaupun pengumpulan sampah memang menjadikan pengurusan memori di Jawa jauh lebih mudah daripada di C atau C ++, anda tidak dapat melupakan memori semasa anda memprogram di Java. Untuk mengetahui kapan anda mungkin perlu memikirkan pengurusan memori di Java, anda perlu mengetahui sedikit tentang cara pengumpulan sampah di dalam spesifikasi Java.

Pengumpulan sampah tidak diamanatkan

Perkara pertama yang perlu diketahui adalah bahawa tidak kira seberapa rajin anda mencari di Spesifikasi Mesin Maya Java (Spesifikasi JVM), anda tidak akan dapat mencari ayat yang memerintahkan, Setiap JVM mesti mempunyai pengumpul sampah. Spesifikasi Mesin Maya Java memberikan perancang VM banyak kelonggaran dalam menentukan bagaimana pelaksanaannya akan menguruskan memori, termasuk memutuskan sama ada menggunakan atau tidak pengumpulan sampah sama sekali. Oleh itu, ada kemungkinan beberapa JVM (seperti kad pintar tanpa tulang) mungkin memerlukan program yang dijalankan pada setiap sesi "sesuai" dalam memori yang ada.

Sudah tentu, anda selalu kehabisan memori, walaupun pada sistem memori maya. Spesifikasi JVM tidak menyatakan berapa banyak memori yang tersedia untuk JVM. Ia hanya menyatakan bahawa setiap kali JVM tidak kehabisan memori, ia perlu membuang sebuah OutOfMemoryError.

Walaupun begitu, untuk memberikan aplikasi Java peluang terbaik untuk dijalankan tanpa kehabisan memori, kebanyakan JVM akan menggunakan pengumpul sampah. Pemungut sampah memperoleh kembali memori yang ditempati oleh objek yang tidak dirujuk di timbunan, sehingga memori dapat digunakan kembali oleh objek baru, dan biasanya menguraikan timbunan ketika program dijalankan.

Algoritma pengumpulan sampah tidak ditentukan

Perintah lain yang tidak anda dapati dalam spesifikasi JVM adalah Semua JVM yang menggunakan pengumpulan sampah mesti menggunakan algoritma XXX. Pereka setiap JVM dapat memutuskan bagaimana pengumpulan sampah akan berfungsi dalam pelaksanaannya. Algoritma pengumpulan sampah adalah satu bidang di mana vendor JVM dapat berusaha untuk menjadikan pelaksanaannya lebih baik daripada persaingan. Ini penting bagi anda sebagai pengaturcara Java kerana alasan berikut:

Oleh kerana anda umumnya tidak tahu bagaimana pengumpulan sampah akan dilakukan di dalam JVM, anda tidak tahu kapan objek tertentu akan dikumpulkan sampah.

Jadi apa? anda mungkin bertanya. Sebab yang mungkin anda ambil berat ketika objek yang dikumpulkan sampah ada kaitannya dengan penyelesai. ( Finalizer didefinisikan sebagai metode instan Java biasa yang dinamakan finalize()tidak berlaku dan tidak ada argumen.) Spesifikasi Java membuat janji berikut tentang finalis:

Sebelum mendapatkan kembali memori yang ditempati oleh objek yang mempunyai pemecah, pengumpul sampah akan meminta pemecah objek tersebut.

Memandangkan anda tidak tahu kapan objek akan dikumpulkan sampah, tetapi anda tahu bahawa objek yang dapat diselesaikan akan diselesaikan kerana ia dikumpulkan sampah, anda boleh membuat pemotongan besar berikut:

Anda tidak tahu bila objek akan dimuktamadkan.

Anda harus mencetak fakta penting ini di otak anda dan selamanya membiarkannya memberitahu reka bentuk objek Java anda.

Penyelesaian yang harus dielakkan

Peraturan utama mengenai pemenang adalah:

Jangan merancang program Java anda sehingga kebenaran bergantung pada penyelesaian "tepat pada masanya".

Dengan kata lain, jangan menulis program yang akan hancur jika objek tertentu tidak diselesaikan oleh titik-titik tertentu dalam kehidupan pelaksanaan program. Sekiranya anda menulis program seperti itu, program ini mungkin berfungsi pada beberapa pelaksanaan JVM tetapi gagal pada yang lain.

Jangan bergantung pada penyelesai untuk melepaskan sumber bukan memori

Contoh objek yang melanggar peraturan ini adalah yang membuka fail dalam konstruktornya dan menutup fail dalam finalize()kaedahnya. Walaupun reka bentuk ini kelihatan kemas, rapi, dan simetris, ia berpotensi menimbulkan bug yang tidak menentu. Program Java pada umumnya hanya akan mempunyai jumlah pengendalian fail yang terhad. Apabila semua pegangan itu digunakan, program tidak akan dapat membuka fail lagi.

Program Java yang menggunakan objek seperti itu (yang membuka fail dalam konstruktornya dan menutupnya dalam penyusunnya) mungkin berfungsi dengan baik pada beberapa implementasi JVM. Pada pelaksanaan seperti itu, penyelesaian akan dilakukan dengan cukup kerap untuk memastikan jumlah pengendalian fail mencukupi setiap masa. Tetapi program yang sama mungkin gagal pada JVM yang berbeza yang pengutip sampahnya tidak selesai cukup kerap agar program tidak berjalan dari pengendalian fail. Atau, yang lebih berbahaya lagi, program ini mungkin berfungsi pada semua pelaksanaan JVM sekarang tetapi gagal dalam situasi kritikal misi beberapa tahun (dan kitaran pelepasan) di jalan.

Peraturan pemenang lain

Dua keputusan lain yang diserahkan kepada pereka JVM adalah memilih utas (atau utas) yang akan melaksanakan penyelesai dan urutan di mana penyusun akan dijalankan. Penyudah boleh dijalankan dalam urutan apa pun - secara berurutan dengan satu utas atau serentak oleh beberapa utas. Sekiranya program anda entah bagaimana bergantung kepada kebenaran pada penyelesai yang dijalankan dalam urutan tertentu, atau oleh utas tertentu, program ini mungkin berfungsi pada beberapa pelaksanaan JVM tetapi gagal pada yang lain.

Anda juga harus ingat bahawa Java menganggap suatu objek akan diselesaikan sama ada finalize()kaedah itu kembali normal atau selesai secara tiba-tiba dengan membuang pengecualian. Pengutip sampah mengabaikan pengecualian yang dilemparkan oleh pemenang dan sama sekali tidak memberitahu aplikasi yang lain bahawa pengecualian telah dilemparkan. Sekiranya anda perlu memastikan bahawa finalis tertentu menyelesaikan misi tertentu, anda mesti menulis finalis tersebut sehingga dapat menangani pengecualian yang mungkin timbul sebelum finalis menyelesaikan misinya.

Satu lagi kaedah praktik mengenai pemecah masalah adalah objek yang tersisa di timbunan pada akhir hayat aplikasi. Secara lalai, pengutip sampah tidak akan melakukan penyelesaikan objek yang tersisa di timbunan semasa aplikasi keluar. Untuk menukar lalai ini, anda mesti menggunakan runFinalizersOnExit()kaedah kelas Runtimeatau System, truesebagai parameter tunggal. Sekiranya program anda mengandungi objek yang pemuktamadnya mesti benar-benar dipanggil sebelum program keluar, pastikan anda memanggil runFinalizersOnExit()suatu tempat dalam program anda.

Oleh itu, apa yang menjadi pilihan akhir?

Sekarang anda mungkin merasakan bahawa anda tidak banyak menggunakan finalis. Walaupun sebahagian besar kelas yang anda rancang tidak akan merangkumi finalis, ada beberapa alasan untuk menggunakan finalis.

Salah satu yang masuk akal, walaupun jarang berlaku, aplikasi untuk menyelesaikan adalah membebaskan memori yang diperuntukkan dengan kaedah asli. Sekiranya objek memanggil kaedah asli yang mengalokasikan memori (mungkin fungsi C yang memanggil malloc()), penyelesai objek itu boleh menggunakan kaedah asli yang membebaskan memori (panggilan free()). Dalam keadaan ini, anda akan menggunakan penyelesai untuk mengosongkan memori yang diperuntukkan bagi pihak objek - memori yang tidak akan diambil semula secara automatik oleh pengumpul sampah.

Satu lagi, yang lebih biasa, penggunaan finalis adalah untuk menyediakan mekanisme penggantian untuk melepaskan sumber yang tidak mempunyai memori seperti pemegang fail atau soket. Seperti yang telah disebutkan sebelumnya, anda tidak boleh bergantung pada penentu untuk melepaskan sumber bukan memori yang terbatas. Sebaliknya, anda harus memberikan kaedah yang akan melepaskan sumbernya. Tetapi anda mungkin juga ingin memasukkan finalisator yang memeriksa untuk memastikan sumbernya telah dilepaskan, dan jika belum, itu terus berjalan dan melepaskannya. Penyudah seperti ini melindungi (dan semoga tidak akan mendorong) penggunaan kelas anda secara tidak senonoh. Sekiranya pengaturcara pelanggan lupa untuk menggunakan kaedah yang anda berikan untuk melepaskan sumber, pemenang akan melepaskan sumber jika objek tersebut pernah dikumpulkan sampah. The finalize()kaedah yangLogFileManager kelas, yang ditunjukkan kemudian dalam artikel ini, adalah contoh penyelesai jenis ini.

Elakkan penyalahgunaan akhir

Kewujudan penyelesaian menghasilkan beberapa komplikasi menarik untuk JVM dan beberapa kemungkinan menarik untuk pengaturcara Java. Apa yang diberikan finalisasi kepada pengaturcara adalah kekuatan hidup dan mati objek. Ringkasnya, adalah mungkin dan sepenuhnya sah di Jawa untuk menghidupkan kembali objek dalam penyudah - untuk menghidupkannya kembali dengan menjadikannya dirujuk lagi. (Salah satu cara yang dapat dicapai oleh finalis adalah dengan menambahkan referensi ke objek yang diselesaikan ke daftar terpautkan statis yang masih "masih hidup.") Walaupun kekuatan seperti itu mungkin menggoda untuk melakukan latihan kerana membuat Anda merasa penting, aturan praktis adalah untuk menahan godaan untuk menggunakan kekuatan ini. Secara umum, menghidupkan semula objek dalam penyusun merupakan penyalahgunaan penyelesai.

Justifikasi utama untuk peraturan ini adalah bahawa setiap program yang menggunakan kebangkitan dapat didesain ulang menjadi program yang lebih mudah difahami yang tidak menggunakan kebangkitan. Bukti formal teorema ini ditinggalkan sebagai latihan kepada pembaca (saya selalu ingin mengatakannya), tetapi dalam semangat tidak formal, anggap bahawa kebangkitan objek akan sama rawak dan tidak dapat diramalkan seperti pemuktamadan objek. Oleh itu, reka bentuk yang menggunakan kebangkitan akan sukar dipahami oleh programmer penyelenggaraan seterusnya yang berlaku - yang mungkin tidak sepenuhnya memahami keistimewaan pengumpulan sampah di Jawa.

Sekiranya anda merasa mesti menghidupkan kembali objek, pertimbangkan untuk mengklon salinan objek baru dan bukannya menghidupkan semula objek lama yang sama. Alasan di sebalik nasihat ini adalah bahawa pengumpul sampah di JVM menggunakan finalize()kaedah objek hanya sekali. Sekiranya objek itu dibangkitkan semula dan tersedia untuk pengumpulan sampah untuk kali kedua, kaedah objek finalize()tersebut tidak akan digunakan lagi.