Apakah menggunakan final untuk variabel di Java meningkatkan pengumpulan sampah?

86

Hari ini kolega saya dan saya berdiskusi tentang penggunaan file final kata kunci di Java untuk meningkatkan pengumpulan sampah.

Misalnya, jika Anda menulis metode seperti:

public Double doCalc(final Double value)
{
   final Double maxWeight = 1000.0;
   final Double totalWeight = maxWeight * value;
   return totalWeight;  
}

Mendeklarasikan variabel dalam metode final akan membantu pengumpulan sampah untuk membersihkan memori dari variabel yang tidak digunakan dalam metode setelah metode keluar.

Apakah ini benar?

Goran Martinic
sumber
sebenarnya ada dua hal di sini. 1) ketika Anda menulis ke bidang metode lokal dan sebuah instance. Saat Anda menulis ke sebuah contoh, mungkin ada manfaatnya.
Eugene

Jawaban:

86

Berikut adalah contoh yang sedikit berbeda, satu dengan bidang jenis referensi akhir daripada variabel lokal jenis nilai akhir:

public class MyClass {

   public final MyOtherObject obj;

}

Setiap kali Anda membuat instance MyClass, Anda akan membuat referensi keluar ke instance MyOtherObject, dan GC harus mengikuti tautan tersebut untuk mencari objek langsung.

JVM menggunakan algoritme GC mark-sweep, yang harus memeriksa semua referensi langsung di lokasi "akar" GC (seperti semua objek dalam tumpukan panggilan saat ini). Setiap benda hidup "ditandai" sebagai hidup, dan setiap benda yang dirujuk oleh benda hidup juga ditandai sebagai hidup.

Setelah menyelesaikan fase mark, GC menyapu heap, membebaskan memori untuk semua objek yang tidak ditandai (dan memadatkan memori untuk objek aktif yang tersisa).

Selain itu, penting untuk mengetahui bahwa memori heap Java dipartisi menjadi "generasi muda" dan "generasi tua". Semua objek pada awalnya dialokasikan untuk generasi muda (kadang-kadang disebut sebagai "kamar bayi"). Karena sebagian besar objek berumur pendek, GC lebih agresif dalam membebaskan sampah terbaru dari generasi muda. Jika sebuah objek bertahan dalam siklus pengumpulan generasi muda, ia akan dipindahkan ke generasi lama (terkadang disebut sebagai "generasi bertenor"), yang lebih jarang diproses.

Jadi, di luar kepala saya, saya akan mengatakan "tidak, modifer 'terakhir' tidak membantu GC mengurangi beban kerjanya".

Menurut pendapat saya, strategi terbaik untuk mengoptimalkan manajemen memori Anda di Java adalah dengan menghilangkan referensi palsu secepat mungkin. Anda dapat melakukannya dengan menetapkan "null" ke referensi objek segera setelah Anda selesai menggunakannya.

Atau, lebih baik lagi, minimalkan ukuran setiap cakupan deklarasi. Misalnya, jika Anda mendeklarasikan sebuah objek di awal metode 1000-baris, dan jika objek tetap hidup hingga penutupan cakupan metode itu (tanda kurung kurawal penutup terakhir), objek mungkin tetap hidup lebih lama dari yang sebenarnya. perlu.

Jika Anda menggunakan metode kecil, dengan hanya selusin baris kode, maka objek yang dideklarasikan di dalam metode itu akan keluar dari cakupan lebih cepat, dan GC akan dapat melakukan sebagian besar pekerjaannya dalam lingkungan yang jauh lebih efisien. generasi muda. Anda tidak ingin objek dipindahkan ke generasi yang lebih tua kecuali benar-benar diperlukan.

benjismith
sumber
Inspirasi. Saya selalu berpikir kode inline lebih cepat, tetapi jika jvm kehabisan memori maka itu akan melambat juga. hmmmmm ...
WolfmanDragon
1
Saya hanya menebak-nebak di sini ... tetapi saya berasumsi bahwa kompiler JIT dapat menyebariskan nilai primitif akhir (bukan objek), untuk peningkatan kinerja sederhana. Sebaliknya, kode sebaris mampu menghasilkan pengoptimalan yang signifikan, tetapi tidak ada hubungannya dengan variabel akhir.
benjismith
2
Tidak mungkin menetapkan null ke objek akhir yang sudah dibuat, lalu mungkin final alih-alih bantuan, bisa membuat segalanya lebih sulit
Hernán Eche
1
Seseorang bisa menggunakan {} untuk membatasi cakupan dalam metode besar juga, daripada memecahnya menjadi beberapa metode privat yang mungkin tidak relevan dengan metode kelas lainnya.
mmm
Anda juga dapat menggunakan variabel lokal sebagai ganti kolom jika memungkinkan untuk meningkatkan pengumpulan sampah memori dan untuk mengurangi hubungan referensial.
sivi
37

Mendeklarasikan variabel lokal finaltidak akan memengaruhi pengumpulan sampah, itu hanya berarti Anda tidak dapat mengubah variabel. Contoh Anda di atas tidak boleh dikompilasi saat Anda memodifikasi variabel totalWeightyang telah ditandai final. Di sisi lain, mendeklarasikan primitif ( doublebukan Double) finalakan memungkinkan variabel tersebut dimasukkan ke dalam kode panggilan, sehingga dapat menyebabkan peningkatan memori dan kinerja. Ini digunakan ketika Anda memiliki nomor public static final Stringsdi kelas.

Secara umum, compiler dan runtime akan mengoptimalkannya jika memungkinkan. Cara terbaik adalah menulis kode dengan tepat dan tidak mencoba terlalu rumit. Gunakan finaljika Anda tidak ingin variabel diubah. Asumsikan bahwa setiap pengoptimalan mudah akan dilakukan oleh kompilator, dan jika Anda khawatir tentang kinerja atau penggunaan memori, gunakan profiler untuk menentukan masalah sebenarnya.

Aaron
sumber
26

Tidak, itu sama sekali tidak benar.

Ingat itu finaltidak berarti konstan, itu hanya berarti Anda tidak dapat mengubah referensi.

final MyObject o = new MyObject();
o.setValue("foo"); // Works just fine
o = new MyObject(); // Doesn't work.

Mungkin ada beberapa pengoptimalan kecil berdasarkan pengetahuan bahwa JVM tidak perlu mengubah referensi (seperti tidak memiliki pemeriksaan untuk melihat apakah telah berubah) tetapi akan sangat kecil sehingga tidak perlu khawatir.

Final harus dianggap sebagai meta-data yang berguna bagi pengembang dan bukan sebagai pengoptimalan kompiler.

SCdF
sumber
17

Beberapa hal yang perlu diperhatikan:

  • Menghapus referensi tidak akan membantu GC. Jika ya, itu akan menunjukkan bahwa variabel Anda memiliki cakupan yang berlebihan. Satu pengecualian adalah kasus nepotisme objek.

  • Belum ada alokasi on-stack di Java.

  • Mendeklarasikan variabel final berarti Anda tidak dapat (dalam kondisi normal) memberikan nilai baru ke variabel tersebut. Karena final tidak mengatakan apa-apa tentang ruang lingkup, itu tidak mengatakan apa pun tentang efeknya pada GC.

Gereja
sumber
Ada alokasi on-stack (dari primitif dan referensi ke objek di heap) di java: stackoverflow.com/a/8061692/32453 Java membutuhkan objek dalam penutupan yang sama seperti kelas / lambda anonim untuk menjadi final juga, tetapi ternyata keluar itu hanya untuk mengurangi "memori yang dibutuhkan / frame" / mengurangi kebingungan jadi tidak terkait dengan pengumpulannya ...
rogerdpack
11

Yah, saya tidak tahu tentang penggunaan pengubah "final" dalam kasus ini, atau pengaruhnya pada GC.

Tetapi saya dapat memberi tahu Anda ini: penggunaan nilai Kotak daripada primitif (misalnya, Double, bukan double) akan mengalokasikan objek tersebut di heap daripada tumpukan, dan akan menghasilkan sampah yang tidak perlu yang harus dibersihkan oleh GC.

Saya hanya menggunakan primitif dalam kotak ketika diperlukan oleh API yang ada, atau ketika saya membutuhkan primatif yang dapat dibatalkan.

benjismith
sumber
1
Kamu benar. Saya hanya membutuhkan contoh singkat untuk menjelaskan pertanyaan saya.
Goran Martinic
5

Variabel akhir tidak dapat diubah setelah tugas awal (diberlakukan oleh kompilator).

Ini tidak mengubah perilaku pengumpulan sampah seperti itu. Satu-satunya masalah adalah bahwa variabel-variabel ini tidak dapat dinihilkan ketika tidak digunakan lagi (yang dapat membantu pengumpulan sampah dalam situasi memori yang ketat).

Anda harus tahu bahwa final memungkinkan compiler membuat asumsi tentang apa yang harus dioptimalkan. Kode sebaris dan tidak termasuk kode yang diketahui tidak dapat dijangkau.

final boolean debug = false;

......

if (debug) {
  System.out.println("DEBUG INFO!");
}

Println tidak akan dimasukkan dalam kode byte.

Thorbjørn Ravn Andersen
sumber
@Eugene Tergantung pada manajer keamanan Anda dan apakah kompilator memasukkan variabel tersebut atau tidak.
Thorbjørn Ravn Andersen
benar, saya hanya bertele-tele; tidak ada lagi; juga memberikan jawaban juga
Eugene
4

Ada kasus sudut yang tidak begitu terkenal dengan pengumpul sampah generasi. (Untuk penjelasan singkat baca jawaban benjismith untuk wawasan yang lebih dalam, baca artikel di bagian akhir).

Gagasan dalam GC generasi adalah bahwa sebagian besar waktu hanya perlu mempertimbangkan generasi muda. Lokasi root dipindai untuk referensi, dan kemudian objek generasi muda dipindai. Selama penyapuan yang lebih sering ini, tidak ada objek di generasi lama yang diperiksa.

Sekarang, masalahnya berasal dari fakta bahwa suatu objek tidak diperbolehkan memiliki referensi ke objek yang lebih muda. Ketika objek berumur panjang (generasi lama) mendapat referensi ke objek baru, referensi itu harus secara eksplisit dilacak oleh pengumpul sampah (lihat artikel dari IBM di hotspot JVM collector ), yang sebenarnya memengaruhi kinerja GC.

Alasan mengapa objek lama tidak bisa merujuk ke yang lebih muda adalah karena, karena objek lama tidak diperiksa dalam koleksi kecil, jika satu-satunya referensi ke objek disimpan di objek lama, itu tidak akan ditandai, dan akan salah. dialokasikan selama tahap penyisiran.

Tentu saja, seperti yang ditunjukkan oleh banyak orang, kata kunci terakhir tidak secara nyata memengaruhi pengumpul sampah, tetapi menjamin bahwa referensi tidak akan pernah diubah menjadi objek yang lebih muda jika objek ini bertahan dari koleksi kecil dan membuatnya ke heap yang lebih lama.

Artikel:

IBM pada pengumpulan sampah: sejarah , JVM hotspot dan kinerja . Ini mungkin tidak lagi sepenuhnya valid, karena sudah ada sejak 2003/04, tetapi memberikan beberapa wawasan yang mudah dibaca tentang GC.

Sun on Tuning pengumpulan sampah

David Rodríguez - dribeas
sumber
3

GC bertindak atas ref yang tidak dapat dijangkau. Ini tidak ada hubungannya dengan "final", yang hanya merupakan penegasan tugas satu kali. Apakah mungkin bahwa beberapa VM GC dapat menggunakan "final"? Saya tidak mengerti bagaimana atau mengapa.

dongilmore
sumber
3

finalpada variabel dan parameter lokal tidak membuat perbedaan pada file kelas yang dihasilkan, sehingga tidak dapat mempengaruhi kinerja runtime. Jika sebuah kelas tidak memiliki subclass, HotSpot memperlakukan kelas itu seolah-olah itu sudah final (ia dapat membatalkannya nanti jika kelas yang melanggar asumsi itu dimuat). Saya percaya finalpada metode hampir sama dengan kelas. finalpada bidang statis memungkinkan variabel diinterpretasikan sebagai "konstanta waktu kompilasi" dan pengoptimalan dilakukan oleh javac atas dasar itu. finalon field memungkinkan JVM memiliki beberapa kebebasan untuk mengabaikan hubungan yang terjadi-sebelum .

Tom Hawtin - tackline
sumber
2

Sepertinya ada banyak jawaban yang hanya merupakan dugaan yang mengembara. Sebenarnya, tidak ada pengubah akhir untuk variabel lokal di tingkat bytecode. Mesin virtual tidak akan pernah tahu bahwa variabel lokal Anda ditetapkan sebagai final atau tidak.

Jawaban atas pertanyaan Anda adalah tidak.

Matt Quigley
sumber
Itu mungkin benar, tetapi penyusun masih dapat menggunakan informasi akhir selama analisis aliran data.
@WernerVanBelle kompilator sudah mengetahui bahwa variabel hanya disetel sekali. Itu harus melakukan analisis aliran data untuk mengetahui bahwa variabel mungkin nihil, tidak diinisialisasi sebelum digunakan, dll. Jadi, final lokal tidak menawarkan informasi baru ke kompilator.
Matt Quigley
Tidak. Analisis aliran data dapat menyimpulkan banyak hal, tetapi dimungkinkan untuk memiliki program lengkap di dalam blok yang akan atau tidak akan menetapkan variabel lokal. Kompilator tidak dapat mengetahui sebelumnya apakah variabel akan ditulis dan akan tetap sama sekali. Oleh karena itu, compiler, tanpa kata kunci terakhir, tidak dapat menjamin bahwa suatu variabel bersifat final atau tidak.
@WernerVanBelle Saya benar-benar tertarik, dapatkah Anda memberi contoh? Saya tidak melihat bagaimana bisa ada tugas akhir untuk variabel non-final yang tidak diketahui oleh compiler. Kompilator mengetahui bahwa jika Anda memiliki variabel yang tidak diinisialisasi, ia tidak akan mengizinkan Anda menggunakannya. Jika Anda mencoba untuk menetapkan variabel terakhir di dalam sebuah loop, compiler tidak akan mengizinkan Anda. Apa contoh di mana variabel yang DAPAT dideklarasikan sebagai final, tetapi TIDAK, dan compiler tidak dapat menjamin bahwa itu adalah final? Saya menduga bahwa setiap contoh akan menjadi variabel yang tidak dapat dideklarasikan sebagai final sejak awal.
Matt Quigley
Ah setelah membaca ulang komentar Anda, saya melihat bahwa contoh Anda adalah variabel yang diinisialisasi di dalam blok bersyarat. Variabel tersebut tidak dapat menjadi final sejak awal, kecuali semua jalur kondisi menginisialisasi variabel satu kali. Jadi, kompilator mengetahui tentang deklarasi semacam itu - begitulah cara mengijinkan Anda untuk mengompilasi variabel final yang diinisialisasi di dua tempat berbeda (pikirkan final int x; if (cond) x=1; else x=2;). Oleh karena itu, compiler tanpa kata kunci terakhir dapat menjamin bahwa suatu variabel bersifat final atau tidak.
Matt Quigley
1

Semua metode dan variabel dapat diganti secara default di subkelas. Jika kita ingin menyimpan subkelas dari menimpa anggota superclass, kita dapat mendeklarasikannya sebagai final menggunakan kata kunci final. Misalnya- final int a=10; final void display(){......} Membuat metode akhir memastikan bahwa fungsionalitas yang ditentukan dalam superclass tidak akan pernah berubah. Demikian pula nilai variabel final tidak pernah bisa diubah. Variabel akhir berperilaku seperti variabel kelas.

Debasish Pakhira
sumber
1

Sebenarnya tentang bidang contoh , final mungkin sedikit meningkatkan kinerja jika GC tertentu ingin memanfaatkannya. Ketika GCterjadi bersamaan (itu berarti aplikasi Anda masih berjalan, sementara GC sedang berlangsung ), lihat ini untuk penjelasan yang lebih luas , GC harus menerapkan hambatan tertentu saat menulis dan / atau membaca selesai. Tautan yang saya berikan cukup banyak menjelaskan hal itu, tetapi untuk membuatnya sangat pendek: ketika aGC melakukan beberapa pekerjaan bersamaan, semua membaca dan menulis ke heap (sementara GC itu sedang berlangsung), "dicegat" dan diterapkan nanti; sehingga fase GC bersamaan dapat menyelesaikan pekerjaannya.

Untuk finalbidang Misalnya, karena mereka tidak dapat diubah (kecuali refleksi), hambatan ini dapat dihilangkan. Dan ini bukan hanya teori murni.

Shenandoah GCmemiliki mereka dalam praktek (meskipun tidak lama ), dan Anda dapat melakukan, misalnya:

-XX:+UnlockExperimentalVMOptions  
-XX:+UseShenandoahGC  
-XX:+ShenandoahOptimizeInstanceFinals

Dan akan ada pengoptimalan dalam algoritma GC yang akan membuatnya sedikit lebih cepat. Ini karena tidak akan ada penghalang yang menghalangi final, karena tidak ada yang boleh memodifikasinya, selamanya. Bahkan tidak melalui refleksi atau JNI.

Eugene
sumber
0

Satu-satunya hal yang dapat saya pikirkan adalah bahwa kompilator dapat mengoptimalkan variabel akhir dan menyebariskannya sebagai konstanta ke dalam kode, sehingga Anda tidak akan mendapatkan alokasi memori.

Sijin
sumber
0

Tentu saja, selama hidup objek lebih pendek yang menghasilkan manfaat besar dari manajemen memori, baru-baru ini kami memeriksa fungsionalitas ekspor yang memiliki variabel instance pada satu pengujian dan pengujian lain yang memiliki variabel lokal level metode. selama pengujian beban, JVM mengeluarkan outofmemoryerror pada pengujian pertama dan JVM dihentikan. Namun pada pengujian kedua, berhasil mendapatkan laporan karena manajemen memori yang lebih baik.

santhosh kumar
sumber
0

Satu-satunya saat saya lebih suka mendeklarasikan variabel lokal sebagai final adalah ketika:

  • Saya harus membuatnya final sehingga dapat dibagikan dengan beberapa kelas anonim (misalnya: membuat utas daemon dan membiarkannya mengakses beberapa nilai dari metode melampirkan)

  • Saya ingin membuatnya final (misalnya: beberapa nilai yang tidak boleh / tidak diganti karena kesalahan)

Apakah mereka membantu pengumpulan sampah dengan cepat?
AFAIK suatu objek menjadi kandidat koleksi GC jika tidak memiliki referensi kuat ke sana dan dalam hal ini juga tidak ada jaminan bahwa mereka akan segera mengumpulkan sampah. Secara umum, referensi yang kuat dikatakan mati ketika keluar dari ruang lingkup atau pengguna secara eksplisit menetapkannya kembali ke referensi nol, dengan demikian, mendeklarasikannya final berarti referensi akan terus ada sampai metode tersebut ada (kecuali ruang lingkupnya secara eksplisit dipersempit menjadi blok dalam tertentu {}) karena Anda tidak dapat menetapkan kembali variabel final (yaitu tidak dapat menetapkan kembali ke nol). Jadi saya pikir 'final' Pengumpulan Sampah wrt dapat menyebabkan kemungkinan penundaan yang tidak diinginkan sehingga seseorang harus sedikit berhati-hati dalam mendefinisikan ruang lingkup karena itu mengontrol kapan mereka akan menjadi kandidat GC.

sactiw
sumber