Apakah mengesampingkan Object.finalize () benar-benar buruk?

34

Dua argumen utama yang menentang penggantian Object.finalize()adalah:

  1. Anda tidak bisa memutuskan kapan itu dipanggil.

  2. Mungkin tidak dipanggil sama sekali.

Jika saya memahami ini dengan benar, saya tidak berpikir itu adalah alasan yang cukup baik untuk membenci Object.finalize()begitu banyak.

  1. Terserah implementasi VM dan GC untuk menentukan kapan waktu yang tepat untuk mendelokasi objek, bukan pengembang. Mengapa penting untuk memutuskan kapan Object.finalize()dipanggil?

  2. Biasanya, dan koreksi saya jika saya salah, satu-satunya waktu Object.finalize()tidak akan dipanggil adalah ketika aplikasi dihentikan sebelum GC mendapat kesempatan untuk berjalan. Namun, objek itu tetap tidak dapat dialokasikan ketika proses aplikasi dihentikan dengannya. Jadi Object.finalize()tidak dipanggil karena tidak perlu dipanggil. Mengapa pengembang peduli?

Setiap kali saya menggunakan objek yang harus saya tutup secara manual (seperti menangani file dan koneksi), saya menjadi sangat frustrasi. Saya harus terus-menerus memeriksa apakah suatu objek memiliki implementasi close(), dan saya yakin saya telah melewatkan beberapa panggilan ke sana di beberapa titik di masa lalu. Mengapa tidak lebih sederhana dan aman untuk menyerahkannya ke VM dan GC untuk membuang objek-objek ini dengan memasukkan close()implementasinya Object.finalize()?

AxiomaticNexus
sumber
1
Juga perhatikan: seperti banyak API dari era Java 1.0, semantik utas finalize()agak kacau. Jika Anda pernah mengimplementasikannya, pastikan itu aman untuk semua metode lain pada objek yang sama.
billc.cn
2
Ketika Anda mendengar orang mengatakan finalizer itu buruk, itu tidak berarti program Anda akan berhenti bekerja jika Anda memilikinya; artinya seluruh gagasan finalisasi tidak berguna.
user253751
1
Beri +1 pada pertanyaan ini. Sebagian besar jawaban di bawah ini menyatakan bahwa sumber daya seperti deskriptor file terbatas dan harus dikumpulkan secara manual. Hal yang sama juga berlaku untuk memori, jadi jika kami menerima beberapa keterlambatan dalam pengumpulan memori, mengapa tidak menerimanya untuk deskriptor file dan / atau sumber daya lainnya?
mbonnin
Mengatasi paragraf terakhir Anda, Anda dapat membiarkannya di Jawa untuk menangani hal-hal penutup seperti menangani file dan koneksi dengan sedikit usaha di pihak Anda. Gunakan blok coba-dengan-sumber daya - disebutkan beberapa kali sudah ada di jawaban dan komentar, tapi saya pikir itu layak diletakkan di sini. Tutorial Oracle untuk hal ini dapat ditemukan di docs.oracle.com/javase/tutorial/essential/exceptions/…
Jeutnarg

Jawaban:

45

Dalam pengalaman saya, ada satu dan hanya satu alasan untuk mengganti Object.finalize(), tetapi itu adalah alasan yang sangat bagus :

Untuk menempatkan kode logging kesalahan finalize()yang memberi tahu Anda jika Anda pernah lupa untuk memohon close().

Analisis statis hanya dapat menangkap kelalaian dalam skenario penggunaan sepele, dan peringatan kompiler yang disebutkan dalam jawaban lain memiliki pandangan sederhana tentang hal-hal yang sebenarnya harus Anda nonaktifkan untuk menyelesaikan segala sesuatu yang non-sepele. (Saya memiliki lebih banyak peringatan yang diaktifkan daripada programmer lain yang saya kenal atau pernah dengar, tapi saya tidak mengaktifkan peringatan bodoh.)

Finalisasi tampaknya menjadi mekanisme yang baik untuk memastikan bahwa sumber daya tidak rusak, tetapi kebanyakan orang melihatnya dengan cara yang benar-benar salah: mereka menganggapnya sebagai mekanisme cadangan alternatif, perlindungan "kesempatan kedua" yang secara otomatis akan menyelamatkan hari dengan membuang sumber daya yang mereka lupa. Ini salah besar . Hanya ada satu cara untuk melakukan sesuatu: Anda selalu menutup semuanya, atau finalisasi selalu menutup semuanya. Tetapi karena finalisasi tidak dapat diandalkan, finalisasi tidak bisa seperti itu.

Jadi, ada skema ini yang saya sebut Pembuangan Wajib , dan menetapkan bahwa programmer bertanggung jawab untuk selalu secara eksplisit menutup segala sesuatu yang mengimplementasikan Closeableatau AutoCloseable. (Pernyataan coba-dengan-sumber daya masih dianggap sebagai penutupan eksplisit.) Tentu saja, programmer mungkin lupa, jadi di situlah finalisasi berperan, tetapi bukan sebagai peri ajaib yang secara ajaib akan membuat segalanya benar pada akhirnya: Jika finalisasi menemukan yang close()tidak dipanggil, itu tidakmencoba untuk memohonnya, justru karena akan ada (dengan kepastian matematika) ada gerombolan programmer n00b yang akan bergantung padanya untuk melakukan pekerjaan yang mereka terlalu malas atau terlalu linglung untuk melakukannya. Jadi, dengan pembuangan wajib, ketika finalisasi menemukan yang close()tidak diminta, itu mencatat pesan kesalahan merah terang, memberitahu programmer dengan huruf besar semua modal untuk memperbaiki dirinya, barang-barangnya.

Sebagai manfaat tambahan, rumor mengatakan bahwa "JVM akan mengabaikan metode finalisasi () sepele (misalnya yang hanya kembali tanpa melakukan apa pun, seperti yang didefinisikan dalam kelas Object)", sehingga dengan pembuangan wajib Anda dapat menghindari semua finalisasi overhead di seluruh sistem Anda ( lihat jawaban alip untuk informasi tentang seberapa buruk overhead ini) dengan mengkode finalize()metode Anda seperti ini:

@Override
protected void finalize() throws Throwable
{
    if( Global.DEBUG && !closed )
    {
        Log.Error( "FORGOT TO CLOSE THIS!" );
    }
    //super.finalize(); see alip's comment on why this should not be invoked.
}

Gagasan di balik ini adalah bahwa Global.DEBUGadalah static finalvariabel yang nilainya diketahui pada waktu kompilasi, jadi jika itu falsemaka kompiler tidak akan memancarkan kode sama sekali untuk seluruh ifpernyataan, yang akan membuat ini finalizer sepele (kosong), yang pada gilirannya berarti bahwa kelas Anda akan diperlakukan seolah-olah tidak memiliki finalizer. (Dalam C # ini akan dilakukan dengan #if DEBUGblok yang bagus , tapi apa yang bisa kita lakukan, ini java, di mana kita membayar kesederhanaan yang jelas dalam kode dengan overhead tambahan di otak.)

Lebih lanjut tentang Pembuangan Wajib, dengan diskusi tambahan tentang membuang sumber daya di dot Net, di sini: michael.gr: Pembuangan wajib vs kekejian "Buang-buang"

Mike Nakis
sumber
2
@MikeNakis Jangan lupa, Closeable didefinisikan sebagai tidak melakukan apa pun jika dipanggil untuk kedua kalinya: docs.oracle.com/javase/7/docs/api/java/io/Closeable.html . Saya akui bahwa saya terkadang mencatat peringatan ketika kelas saya ditutup dua kali, tetapi secara teknis Anda bahkan tidak seharusnya melakukannya. Namun secara teknis, memanggil .close () di Closable lebih dari sekali benar-benar valid.
Patrick M
1
@ usr semuanya bermuara pada apakah Anda mempercayai pengujian Anda atau Anda tidak mempercayai pengujian Anda. Jika Anda tidak mempercayai pengujian Anda, tentu saja, lanjutkan dan menderita overhead finalisasi juga close() , untuk berjaga-jaga. Saya percaya bahwa jika pengujian saya tidak dapat dipercaya, maka saya lebih baik tidak merilis sistem untuk produksi.
Mike Nakis
3
@ Mike, agar if( Global.DEBUG && ...konstruk berfungsi sehingga JVM akan mengabaikan finalize()metode ini sebagai sepele, Global.DEBUGharus ditetapkan pada waktu kompilasi (sebagai lawan disuntikkan dll) sehingga yang berikut ini akan menjadi kode mati. Panggilan ke super.finalize()luar blok if juga cukup bagi JVM untuk memperlakukannya sebagai non-sepele (berapapun nilainya Global.DEBUG, setidaknya pada HotSpot 1.8) bahkan jika kelas supernya #finalize()juga sepele!
alip
1
@ Mike Aku takut itu masalahnya. Saya mengujinya dengan (versi yang sedikit dimodifikasi) dari tes di artikel yang Anda tautkan , dan verbose hasil GC (bersama-sama dengan kinerja yang sangat buruk) mengkonfirmasi bahwa objek disalin ke survivor / ruang generasi lama dan membutuhkan GC tumpukan penuh untuk mendapatkan menyingkirkan.
alip
1
Apa yang sering diabaikan, adalah risiko pengumpulan objek yang lebih awal dari yang diperkirakan, yang membuat membebaskan sumber daya dalam finalizer merupakan tindakan yang sangat berbahaya. Sebelum ke Java 9, satu-satunya cara untuk memastikan bahwa finalizer tidak menutup sumber daya saat masih digunakan, adalah untuk menyinkronkan objek, dalam keduanya, finalizer dan metode yang menggunakan sumber daya. Itu sebabnya ini bekerja java.io. Jika pengaman benang semacam itu tidak ada dalam daftar keinginan, itu menambah overhead yang disebabkan oleh finalize()...
Holger
28

Setiap kali saya menggunakan objek yang harus saya tutup secara manual (seperti menangani file dan koneksi), saya menjadi sangat frustrasi. [...] Mengapa bukan hanya lebih sederhana dan lebih aman untuk menyerahkannya pada VM dan GC untuk membuang objek-objek ini dengan memasukkan close()implementasinya Object.finalize()?

Karena penanganan file & koneksi (yaitu, deskriptor file pada sistem Linux & POSIX) adalah sumber daya yang langka (Anda mungkin terbatas pada 256 di antaranya pada beberapa sistem, atau 16.384 pada beberapa sistem lain, lihat setrlimit (2) ). Tidak ada jaminan bahwa GC akan dipanggil cukup sering (atau pada waktu yang tepat) untuk menghindari melelahkan sumber daya yang terbatas. Dan jika GC tidak disebut cukup (atau finalisasi tidak berjalan pada waktu yang tepat) Anda akan mencapai batas (mungkin rendah) itu.

Finalisasi adalah hal "upaya terbaik" di JVM. Itu mungkin tidak dipanggil, atau disebut sangat terlambat ... Secara khusus, jika Anda memiliki banyak RAM atau jika program Anda tidak mengalokasikan banyak objek (atau jika sebagian besar dari mereka mati sebelum diteruskan ke beberapa yang cukup tua) generasi dengan menyalin generational GC), GC bisa disebut sangat jarang, dan finalisasi mungkin tidak berjalan sangat sering (atau bahkan mungkin tidak berjalan sama sekali).

Jadi closedeskriptor file secara eksplisit, jika mungkin. Jika Anda takut bocor, gunakan finalisasi sebagai langkah ekstra, bukan yang utama.

Basile Starynkevitch
sumber
7
Saya akan menambahkan bahwa menutup aliran ke file atau soket biasanya mengguyurnya. Membiarkan stream terbuka dengan sia-sia meningkatkan risiko kehilangan data jika daya padam, koneksi terputus (ini juga risiko file diakses melalui jaringan), dll.
2
Sebenarnya, sementara jumlah pengarsiptor yang diizinkan mungkin rendah, itu bukan titik yang benar - benar lengket, karena orang dapat menggunakannya sebagai sinyal untuk GC setidaknya. Hal yang benar-benar bermasalah adalah a) sepenuhnya tidak transparan dengan GC tentang seberapa banyak sumber daya yang tidak dikelola dengan GC bergantung padanya, dan b) banyak dari sumber daya itu unik, sehingga yang lain mungkin akan diblokir atau ditolak.
Deduplicator
2
Dan jika Anda membiarkan file terbuka, itu mungkin mengganggu orang lain yang menggunakannya. (Windows 8 XPS viewer, saya melihat Anda!)
Loren Pechtel
2
"Jika Anda takut bocor [deskriptor file], gunakan finalisasi sebagai tindakan tambahan, bukan sebagai yang utama." Pernyataan ini kedengarannya mencurigakan bagi saya. Jika Anda mendesain kode Anda dengan baik, haruskah Anda benar-benar memperkenalkan kode berlebihan yang menyebarkan pembersihan ke beberapa tempat?
mucaho
2
@BasileStarynkevitch Maksud Anda adalah bahwa di dunia yang ideal, ya, redundansi buruk, tetapi dalam praktiknya, di mana Anda tidak dapat melihat semua aspek yang relevan, lebih baik aman daripada menyesal?
mucaho
13

Lihatlah masalah ini dengan cara ini: Anda hanya boleh menulis kode yang (a) benar (jika tidak, program Anda salah) dan (b) perlu (jika kode Anda terlalu besar, yang berarti lebih banyak RAM diperlukan, lebih banyak siklus dihabiskan untuk hal-hal yang tidak berguna, lebih banyak upaya untuk memahaminya, lebih banyak waktu dihabiskan untuk mempertahankannya dll.)

Sekarang pertimbangkan apa yang ingin Anda lakukan dalam finalizer. Entah itu perlu. Dalam hal ini Anda tidak dapat memasukkannya ke finalizer, karena Anda tidak tahu apakah itu akan dipanggil. Itu tidak cukup baik. Atau itu tidak perlu - maka Anda tidak harus menulis itu di tempat pertama! Either way, memasukkannya ke finalizer adalah pilihan yang salah.

(Perhatikan bahwa contoh Anda nama, seperti menutup file stream, lihat seolah-olah mereka tidak benar-benar diperlukan, tetapi mereka. Hanya saja tidak sampai Anda menekan batas untuk menangani file yang terbuka pada sistem Anda, Anda akan melihat bahwa Anda kode tidak benar. Tapi batas itu adalah fitur dari sistem operasi dan karena itu lebih tak terduga dari kebijakan JVM untuk finalizers, sehingga benar-benar, benar-benar adalah penting bahwa Anda tidak membuang menangani file.)

Kilian Foth
sumber
Jika saya hanya harus menulis kode yang "perlu" maka haruskah saya menghindari semua gaya di semua aplikasi GUI saya? Tentunya tidak ada yang diperlukan; GUI akan bekerja dengan baik tanpa gaya, mereka hanya akan terlihat mengerikan. Tentang finalizer, dapat diperlukan untuk melakukan sesuatu, tetapi masih ok untuk meletakkannya di finalizer, karena finalizer akan dipanggil selama objek GC, jika sama sekali. Jika Anda perlu menutup sumber daya, khususnya bila sudah siap untuk pengumpulan sampah, maka finalizer optimal. Program berhenti melepaskan sumber daya Anda atau finalizer disebut.
Kröw
Jika saya memiliki RAM yang berat, mahal untuk dibuat, objek yang dapat ditutup dan saya memutuskan untuk merujuknya dengan lemah, sehingga dapat dihapus ketika tidak diperlukan, maka saya dapat menggunakan finalize()untuk menutupnya jika siklus gc benar-benar perlu membebaskan RAM Kalau tidak, saya akan menyimpan objek dalam RAM daripada harus membuat ulang dan menutupnya setiap kali saya perlu menggunakannya. Tentu, sumber daya yang dibuka tidak akan dibebaskan sampai objek GCed, kapan pun itu, tetapi saya mungkin tidak perlu menjamin bahwa sumber daya saya akan dibebaskan pada waktu tertentu.
Kröw
8

Salah satu alasan terbesar untuk tidak bergantung pada finalizer adalah bahwa sebagian besar sumber daya yang mungkin tergoda untuk dibersihkan oleh finalizer sangat terbatas. Pengumpul sampah hanya berjalan sesering mungkin, karena melintasi referensi untuk menentukan apakah sesuatu dapat dikeluarkan mahal atau tidak. Ini berarti bahwa itu bisa 'sementara' sebelum benda Anda benar-benar dihancurkan. Jika Anda memiliki banyak objek yang membuka koneksi database berumur pendek, misalnya, membiarkan finalizer untuk membersihkan koneksi ini dapat menghabiskan kumpulan koneksi Anda sementara menunggu pengumpul sampah untuk akhirnya menjalankan dan membebaskan koneksi yang sudah selesai. Kemudian, karena menunggu, Anda berakhir dengan tumpukan besar permintaan yang antri, yang dengan cepat menghabiskan kumpulan koneksi lagi. Saya t'

Selain itu, penggunaan coba-dengan-sumber daya membuat menutup objek 'tertutup' pada penyelesaian mudah dilakukan. Jika Anda tidak terbiasa dengan konstruk ini, saya sarankan Anda memeriksanya: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

Anjing
sumber
8

Selain mengapa menyerahkannya kepada finalizer untuk melepaskan sumber daya umumnya merupakan ide yang buruk, objek yang dapat diselesaikan dilengkapi dengan overhead kinerja.

Dari teori dan praktik Jawa: Pengumpulan dan kinerja sampah (Brian Goetz), Penyelesai bukan teman Anda :

Objek dengan finalizer (yang memiliki metode finalize () non-trivial) memiliki overhead yang signifikan dibandingkan dengan objek tanpa finalizer, dan harus digunakan dengan hemat. Objek yang dapat diselesaikan adalah lebih lambat untuk dialokasikan dan lebih lambat untuk dikumpulkan. Pada waktu alokasi, JVM harus mendaftarkan benda yang dapat diselesaikan dengan pengumpul sampah, dan (setidaknya dalam implementasi JVM HotSpot) objek yang dapat diselesaikan harus mengikuti jalur alokasi yang lebih lambat daripada kebanyakan benda lain. Demikian pula, objek yang dapat diselesaikan lebih lambat untuk dikoleksi juga. Dibutuhkan setidaknya dua siklus pengumpulan sampah (dalam kasus terbaik) sebelum benda yang dapat diselesaikan dapat direklamasi, dan pengumpul sampah harus melakukan pekerjaan ekstra untuk memanggil finalizer. Hasilnya adalah lebih banyak waktu yang dihabiskan untuk mengalokasikan dan mengumpulkan benda-benda dan lebih banyak tekanan pada pengumpul sampah, karena memori yang digunakan oleh objek yang tidak dapat diselesaikan dapat dipertahankan lebih lama. Gabungkan bahwa dengan fakta bahwa finalizer tidak dijamin untuk berjalan dalam jangka waktu yang dapat diprediksi, atau bahkan sama sekali, dan Anda dapat melihat bahwa ada beberapa situasi yang relatif sedikit dimana finalisasi adalah alat yang tepat untuk digunakan.

alip
sumber
Poin luar biasa. Lihat jawaban saya yang menyediakan sarana untuk menghindari overhead kinerja finalize().
Mike Nakis
7

Alasan favorit saya (paling tidak) untuk menghindarinya Object.finalizeadalah bukan karena objek mungkin selesai setelah Anda harapkan, tetapi mereka bisa diselesaikan sebelum Anda mungkin mengharapkannya. Masalahnya bukan bahwa objek yang masih dalam cakupan dapat diselesaikan sebelum lingkup keluar jika Java memutuskan itu tidak lagi dapat dijangkau.

void test() {
   HasFinalize myObject = ...;
   OutputStream os = myObject.stream;

   // myObject is no-longer reachable at this point, 
   // even though it is in scope. But objects are finalized
   // based on reachability.
   // And since finalization is on another thread, it 
   // could happen before or in the middle of the write .. 
   // closing the stream and causing much fun.
   os.write("Hello World");
}

Lihat pertanyaan ini untuk lebih jelasnya. Yang lebih menyenangkan adalah bahwa keputusan ini mungkin hanya dibuat setelah optimasi hot-spot dimulai, membuat ini menyakitkan untuk di-debug.

Michael Anderson
sumber
1
Masalahnya adalah bahwa HasFinalize.streamitu sendiri harus menjadi objek yang dapat diselesaikan secara terpisah. Artinya, finalisasi HasFinalizeseharusnya tidak menyelesaikan atau mencoba membersihkan stream. Atau jika harus, maka itu harus membuat streamtidak dapat diakses.
acelent
4

Saya harus terus-menerus memeriksa apakah suatu objek memiliki implementasi close (), dan saya yakin saya telah melewatkan beberapa panggilan ke sana di beberapa titik di masa lalu.

Di Eclipse, saya mendapat peringatan setiap kali saya lupa untuk menutup sesuatu yang mengimplementasikan Closeable/ AutoCloseable. Saya tidak yakin apakah itu suatu hal Eclipse atau apakah itu bagian dari kompiler resmi, tetapi Anda mungkin melihat ke dalam menggunakan alat analisis statis serupa untuk membantu Anda di sana. FindBugs, misalnya, mungkin dapat membantu Anda memeriksa apakah Anda lupa menutup sumber daya.

Nebu Pookins
sumber
1
Penyebutan ide bagus AutoCloseable. Itu membuat otak-mati-mudah untuk mengelola sumber daya dengan coba-dengan-sumber daya. Ini membatalkan beberapa argumen dalam pertanyaan.
2

Untuk pertanyaan pertama Anda:

Terserah implementasi VM dan GC untuk menentukan kapan waktu yang tepat untuk mendelokasi objek, bukan pengembang. Mengapa penting untuk memutuskan kapan Object.finalize()dipanggil?

Nah, JVM akan menentukan, kapan saat yang tepat untuk merebut kembali penyimpanan yang telah dialokasikan untuk suatu objek. Ini belum tentu waktu ketika sumber daya dibersihkan, Anda ingin melakukan finalize(), harus terjadi. Ini diilustrasikan dalam pertanyaan "finalisasi () dipanggil pada objek yang sangat dapat dijangkau di Java 8" pada SO. Di sana, suatu close()metode telah dipanggil oleh suatu finalize()metode, sementara upaya untuk membaca dari aliran oleh objek yang sama masih tertunda. Jadi selain kemungkinan terkenal yang finalize()dipanggil terlambat, ada kemungkinan dipanggil terlalu dini.

Premis dari pertanyaan kedua Anda:

Biasanya, dan koreksi saya jika saya salah, satu-satunya waktu Object.finalize()tidak akan dipanggil adalah ketika aplikasi dihentikan sebelum GC mendapat kesempatan untuk berjalan.

itu salah. Tidak ada persyaratan untuk JVM untuk mendukung finalisasi sama sekali. Yah, itu tidak sepenuhnya salah karena Anda masih bisa menafsirkannya sebagai "aplikasi dihentikan sebelum finalisasi berlangsung", dengan asumsi bahwa aplikasi Anda akan pernah berakhir.

Tetapi perhatikan perbedaan kecil antara "GC" dari pernyataan asli Anda dan istilah "finalisasi". Pengumpulan sampah berbeda dengan finalisasi. Setelah manajemen memori mendeteksi bahwa suatu objek tidak dapat dijangkau, ia dapat dengan mudah mendapatkan kembali ruangnya, jika salah satunya, ia tidak memiliki finalize()metode khusus atau finalisasi tidak didukung, atau mungkin membuat objek untuk difinalisasi selesai. Dengan demikian, penyelesaian siklus pengumpulan sampah tidak menyiratkan bahwa penyelesai dieksekusi. Itu mungkin terjadi di lain waktu, ketika antrian diproses, atau tidak pernah sama sekali.

Poin ini juga menjadi alasan mengapa bahkan pada JVM dengan dukungan finalisasi, mengandalkannya untuk pembersihan sumber daya adalah berbahaya. Pengumpulan sampah adalah bagian dari manajemen memori dan karenanya dipicu oleh kebutuhan memori. Mungkin saja pengumpulan sampah tidak pernah berjalan karena ada cukup memori selama runtime keseluruhan (yah, itu masih cocok dengan "aplikasi dihentikan sebelum GC mendapat kesempatan untuk menjalankan" deskripsi, entah bagaimana). Ada kemungkinan juga bahwa GC berjalan tetapi setelah itu, ada cukup banyak memori yang direklamasi, sehingga antrian finalizer tidak diproses.

Dengan kata lain, sumber daya asli yang dikelola dengan cara ini masih merupakan hal asing bagi manajemen memori. Meskipun dijamin bahwa OutOfMemoryErrorhanya dibuang setelah upaya yang cukup untuk mengosongkan memori, itu tidak berlaku untuk sumber daya asli dan finalisasi. Ada kemungkinan bahwa membuka file gagal karena sumber daya tidak mencukupi sementara antrian finalisasi penuh dengan objek yang dapat membebaskan sumber daya ini, jika pemodal pernah berlari ...

Holger
sumber