Mekanisme pengumpulan sampah pada platform Java telah meningkatkan efisiensi pengembang secara signifikan, namun pengumpul sampah yang diterapkan dengan buruk dapat menghabiskan terlalu banyak sumber daya aplikasi. Di bagian ketiga dari seri pengoptimalan kinerja mesin virtual Java, Eva Andreasson memperkenalkan model memori dan mekanisme pengumpulan sampah platform Java kepada pemula Java. Dia menjelaskan mengapa fragmentasi (bukan pengumpulan sampah) adalah masalah utama dalam kinerja aplikasi Java, dan mengapa pengumpulan dan pemadatan sampah secara generasi saat ini menjadi cara utama (tetapi bukan yang paling inovatif) untuk menangani fragmentasi aplikasi Java.
Tujuan dari pengumpulan sampah (GC) adalah untuk melepaskan memori yang ditempati oleh objek Java yang tidak lagi direferensikan oleh objek aktif mana pun. Ini adalah bagian inti dari mekanisme manajemen memori dinamis mesin virtual Java. Selama siklus pengumpulan sampah pada umumnya, semua objek yang masih direferensikan (dan oleh karena itu dapat dijangkau) dipertahankan, sedangkan objek yang tidak lagi direferensikan akan dilepaskan dan ruang yang ditempati diambil kembali untuk dialokasikan ke objek baru.
Untuk memahami mekanisme pengumpulan sampah dan berbagai algoritma pengumpulan sampah, Anda perlu mengetahui terlebih dahulu sesuatu tentang model memori platform Java.
Pengumpulan sampah dan model memori platform Java
Saat Anda memulai program Java dari baris perintah dan menentukan parameter startup -Xmx (misalnya: java -Xmx:2g MyApp), memori dengan ukuran yang ditentukan dialokasikan ke proses Java, yang disebut Java heap . Ruang alamat memori khusus ini digunakan untuk menyimpan objek yang dibuat oleh program Java (dan terkadang JVM). Saat aplikasi berjalan dan terus mengalokasikan memori untuk objek baru, tumpukan Java (yaitu, ruang alamat memori khusus) akan terisi secara perlahan.
Pada akhirnya tumpukan Java akan terisi, yang berarti bahwa thread alokasi memori tidak dapat menemukan ruang berdekatan yang cukup besar untuk mengalokasikan memori untuk objek baru. Pada saat ini, JVM memutuskan untuk memberi tahu pengumpul sampah dan memulai pengumpulan sampah. Pengumpulan sampah juga dapat dipicu dengan memanggil System.gc() dalam program, namun penggunaan System.gc() tidak menjamin pengumpulan sampah akan dilakukan. Sebelum pengumpulan sampah apa pun, mekanisme pengumpulan sampah terlebih dahulu akan menentukan apakah aman untuk melakukan pengumpulan sampah. Ketika semua thread aktif aplikasi berada pada titik aman, pengumpulan sampah dapat dimulai. Misalnya, pengumpulan sampah tidak dapat dilakukan saat memori dialokasikan untuk suatu objek, atau pengumpulan sampah tidak dapat dilakukan saat instruksi CPU sedang dioptimalkan, karena konteksnya mungkin hilang dan hasil akhirnya salah.
Pengumpul sampah tidak dapat mengambil kembali objek apa pun dengan referensi aktif, yang akan merusak spesifikasi mesin virtual Java. Benda mati tidak perlu segera didaur ulang, karena benda mati pada akhirnya akan didaur ulang melalui pengumpulan sampah berikutnya. Meskipun ada banyak cara untuk melaksanakan pengumpulan sampah, kedua poin di atas sama untuk semua pelaksanaan pengumpulan sampah. Tantangan sebenarnya dari pengumpulan sampah adalah bagaimana mengidentifikasi apakah suatu objek masih hidup dan bagaimana mendapatkan kembali memori tanpa mempengaruhi aplikasi sebanyak mungkin. Oleh karena itu, pengumpul sampah memiliki dua tujuan berikut:
1. Lepaskan memori yang tidak direferensikan dengan cepat untuk memenuhi kebutuhan alokasi memori aplikasi untuk menghindari kelebihan memori.
2. Meminimalkan dampak terhadap kinerja aplikasi yang berjalan (latensi dan throughput) saat mengambil kembali memori.
Dua jenis pengumpulan sampah
Pada artikel pertama seri ini, saya memperkenalkan dua metode pengumpulan sampah, yaitu penghitungan referensi dan pengumpulan pelacakan. Selanjutnya kami mengeksplorasi kedua pendekatan ini lebih jauh dan memperkenalkan beberapa algoritma pengumpulan jejak yang digunakan dalam lingkungan produksi.
Kolektor penghitungan referensi
Pengumpul penghitungan referensi mencatat jumlah referensi yang menunjuk ke setiap objek Java. Setelah jumlah referensi yang menunjuk ke suatu objek mencapai 0, objek tersebut dapat segera didaur ulang. Kedekatan ini adalah keuntungan utama dari kolektor penghitungan referensi, dan hampir tidak ada biaya tambahan dalam memelihara memori yang tidak ada referensinya, namun melacak jumlah referensi terbaru untuk setiap objek itu mahal.
Kesulitan utama dari pengumpul penghitungan referensi adalah bagaimana memastikan keakuratan penghitungan referensi. Kesulitan lain yang umum adalah bagaimana menangani referensi melingkar. Jika dua objek saling mereferensikan dan tidak direferensikan oleh objek hidup lainnya, maka memori kedua objek tersebut tidak akan pernah diperoleh kembali karena jumlah referensi yang menunjuk ke kedua objek tersebut adalah 0. Daur ulang memori struktur referensi melingkar memerlukan analisis besar (Catatan Penerjemah: Analisis Global di Java Heap), yang akan meningkatkan kompleksitas algoritme dan dengan demikian membawa overhead tambahan ke aplikasi.
kolektor jejak
Kolektor penelusuran didasarkan pada asumsi bahwa semua benda hidup dapat ditemukan dengan mengulangi referensi (referensi dan referensi referensi) ke kumpulan awal benda hidup yang diketahui. Kumpulan awal objek aktif (juga disebut objek root) dapat ditentukan dengan menganalisis register, objek global, dan bingkai tumpukan. Setelah menentukan kumpulan objek awal, kolektor pelacakan mengikuti hubungan referensi objek tersebut dan menandai objek yang ditunjuk oleh referensi sebagai objek aktif secara berurutan, sehingga kumpulan objek aktif yang diketahui terus bertambah. Proses ini berlanjut hingga semua objek yang direferensikan ditandai sebagai objek hidup, dan memori objek yang belum ditandai diperoleh kembali.
Kolektor pelacakan berbeda dari kolektor penghitungan referensi terutama karena ia dapat menangani struktur referensi melingkar. Kebanyakan kolektor penelusuran menemukan objek yang tidak direferensikan dalam struktur referensi melingkar selama fase penandaan.
Tracing collector adalah metode manajemen memori yang paling umum digunakan dalam bahasa dinamis dan saat ini merupakan metode yang paling umum di Java. Metode ini juga telah diverifikasi di lingkungan produksi selama bertahun-tahun. Di bawah ini saya akan memperkenalkan trace collector dimulai dengan beberapa algoritma untuk mengimplementasikan trace collection.
Algoritma pengumpulan jejak
Menyalin pengumpul sampah dan pengumpul sampah mark-sweep bukanlah hal baru, namun keduanya masih merupakan dua algoritme paling umum untuk mengimplementasikan pengumpulan pelacakan saat ini.
Menyalin pengumpul sampah
Pengumpul sampah penyalinan tradisional menggunakan dua ruang alamat di heap (yaitu, dari ruang dan ke ruang). Ketika pengumpulan sampah dilakukan, objek aktif di ruang dari disalin ke ruang ke dihapus (Catatan Penerjemah: Setelah menyalin ke ruang ke atau generasi lama), seluruh ruang dapat didaur ulang. Ketika ruang dialokasikan lagi, ruang ke akan digunakan terlebih dahulu (Catatan Penerjemah: Yaitu ruang ke babak sebelumnya akan digunakan sebagai babak baru dari luar angkasa).
Pada awal implementasi algoritma ini, posisi dari ruang dan ruang ke ruang terus berubah. Artinya, ketika ruang ke penuh dan pengumpulan sampah dipicu, ruang ke menjadi ruang dari, seperti yang ditunjukkan pada Gambar 1. .
Gambar 1 Urutan pengumpulan sampah salinan tradisional
Algoritme penyalinan terbaru memungkinkan setiap ruang alamat di heap digunakan sebagai ruang dan dari ruang. Dengan cara ini mereka tidak perlu bertukar posisi satu sama lain, namun cukup bertukar posisi secara logis.
Keuntungan dari copy collector adalah objek-objek yang disalin pada to space tersusun rapat dan tidak ada fragmentasi sama sekali. Fragmentasi merupakan permasalahan umum yang dihadapi oleh pemulung lainnya, dan juga merupakan permasalahan utama yang akan saya bahas nanti.
Kerugian dari pengumpul salinan
Secara umum, copy collector bersifat stop-the-world, yang berarti selama pengumpulan sampah masih berlangsung, aplikasi tidak dapat dijalankan. Dengan implementasi ini, semakin banyak hal yang perlu Anda salin, semakin besar dampaknya terhadap kinerja aplikasi. Ini merupakan kerugian bagi aplikasi yang sensitif terhadap waktu respons. Saat menggunakan copy collector, Anda juga perlu mempertimbangkan skenario terburuk (yaitu, semua objek di ruang dari adalah objek aktif. Saat ini, Anda perlu menyiapkan ruang yang cukup besar untuk memindahkan objek aktif tersebut, sehingga dapat ruang harus cukup besar untuk memasang semua benda di luar angkasa. Karena keterbatasan ini, pemanfaatan memori dari algoritma penyalinan sedikit tidak mencukupi (Catatan Penerjemah: Dalam kasus terburuk, ruang to harus berukuran sama dengan ruang dari, jadi pemanfaatannya hanya 50%).
kolektor yang jelas tandanya
Sebagian besar JVM komersial yang diterapkan di lingkungan produksi perusahaan menggunakan pengumpul mark-sweep (atau mark) karena tidak mereplikasi dampak pengumpul sampah pada kinerja aplikasi. Kolektor merek yang paling terkenal adalah CMS, G1, GenPar, dan DeterministicGC.
Kolektor mark-sweep melacak referensi objek dan menandai setiap objek yang ditemukan sebagai objek aktif menggunakan bit bendera. Bendera ini biasanya sesuai dengan alamat atau sekelompok alamat di heap. Misalnya: bit aktif dapat berupa bit pada header objek (Catatan Penerjemah: bit) atau bit vektor atau bitmap.
Setelah penandaan selesai, tahap pembersihan dimulai. Fase pembersihan biasanya melintasi heap lagi (tidak hanya objek yang ditandai aktif, tetapi seluruh heap) untuk menemukan ruang alamat memori bersebelahan yang tidak ditandai (memori yang tidak ditandai bebas dan dapat didaur ulang), lalu kolektor Mengaturnya ke dalam daftar bebas. Pengumpul sampah dapat memiliki beberapa daftar gratis (biasanya dibagi berdasarkan ukuran blok memori). Beberapa pengumpul JVM (misalnya: JRockit Real Time) bahkan secara dinamis membagi daftar gratis berdasarkan analisis kinerja aplikasi dan statistik ukuran objek.
Setelah tahap pembersihan, aplikasi dapat mengalokasikan memori kembali. Saat mengalokasikan memori untuk objek baru dari daftar gratis, blok memori yang baru dialokasikan harus sesuai dengan ukuran objek baru, atau ukuran rata-rata objek thread, atau ukuran TLAB aplikasi. Menemukan blok memori dengan ukuran yang tepat untuk objek baru membantu mengoptimalkan memori dan mengurangi fragmentasi.
Tandai - Hapus Cacat Kolektor
Waktu eksekusi fase penandaan bergantung pada jumlah objek aktif di heap, sedangkan waktu eksekusi fase pembersihan bergantung pada ukuran heap. Oleh karena itu, untuk situasi di mana pengaturan heap besar dan terdapat banyak objek aktif di heap, algoritma mark-sweep akan memiliki waktu jeda tertentu.
Untuk aplikasi yang membutuhkan banyak memori, Anda dapat menyesuaikan parameter pengumpulan sampah agar sesuai dengan berbagai skenario dan kebutuhan aplikasi. Dalam banyak kasus, penyesuaian ini setidaknya menunda risiko yang ditimbulkan oleh fase penandaan/sapuan pada SLA perjanjian aplikasi atau layanan (SLA di sini mengacu pada waktu respons yang perlu dicapai oleh aplikasi). Namun penyetelan hanya efektif untuk beban tertentu dan tingkat alokasi memori. Perubahan beban atau modifikasi pada aplikasi itu sendiri memerlukan penyetelan ulang.
Implementasi kolektor sapu-tanda
Setidaknya ada dua metode yang terbukti secara komersial untuk menerapkan pengumpulan sampah sapu bersih. Salah satunya adalah pengumpulan sampah paralel dan yang lainnya adalah pengumpulan sampah secara bersamaan (atau sering kali bersamaan).
Kolektor paralel
Pengumpulan paralel berarti sumber daya digunakan secara paralel oleh rangkaian pengumpulan sampah. Sebagian besar implementasi komersial dari pengumpulan paralel adalah pengumpul stop-the-world, di mana semua thread aplikasi dijeda hingga pengumpulan sampah selesai. Karena pengumpul sampah dapat menggunakan sumber daya secara efisien, mereka biasanya berkinerja lebih baik dalam tolok ukur throughput SPESIFIKASIjbb. Jika throughput sangat penting bagi aplikasi Anda, pengumpul sampah paralel adalah pilihan yang baik.
Kerugian utama dari pengumpulan paralel (terutama untuk lingkungan produksi) adalah bahwa thread aplikasi tidak dapat berfungsi dengan baik selama pengumpulan sampah, seperti halnya pengumpul penyalinan. Oleh karena itu, penggunaan kolektor paralel akan berdampak signifikan pada aplikasi yang sensitif terhadap waktu respons. Terutama ketika terdapat banyak struktur objek aktif yang kompleks di ruang heap, terdapat banyak referensi objek yang perlu dilacak. (Ingat waktu yang dibutuhkan kolektor sapu-tanda untuk mendapatkan kembali memori bergantung pada waktu yang diperlukan untuk melacak kumpulan objek hidup ditambah waktu yang diperlukan untuk melintasi seluruh heap) Dengan pendekatan paralel, aplikasi dijeda untuk sementara waktu. seluruh waktu pengumpulan sampah.
kolektor bersamaan
Pengumpul sampah secara bersamaan lebih cocok untuk aplikasi yang sensitif terhadap waktu respons. Konkurensi berarti thread pengumpulan sampah dan thread aplikasi dijalankan secara bersamaan. Thread pengumpulan sampah tidak memiliki semua sumber daya, sehingga perlu memutuskan kapan memulai pengumpulan sampah, memberikan cukup waktu untuk melacak pengumpulan objek aktif dan mendapatkan kembali memori sebelum memori aplikasi meluap. Jika pengumpulan sampah tidak selesai tepat waktu, aplikasi akan memunculkan kesalahan memory overflow. Sebaliknya, Anda tidak ingin pengumpulan sampah memakan waktu terlalu lama karena akan menghabiskan sumber daya aplikasi dan mempengaruhi throughput. Menjaga keseimbangan ini memerlukan keterampilan, sehingga heuristik digunakan dalam menentukan kapan memulai pengumpulan sampah dan kapan memilih optimalisasi pengumpulan sampah.
Kesulitan lainnya adalah menentukan kapan waktu yang aman untuk melakukan beberapa operasi (operasi yang memerlukan snapshot heap yang lengkap dan akurat), seperti perlu mengetahui kapan fase penandaan selesai sehingga fase pembersihan dapat dimasukkan. Ini bukan masalah bagi kolektor paralel stop-the-world, karena dunia sudah dijeda (Catatan Penerjemah: Thread aplikasi dijeda, dan thread pengumpulan sampah memonopoli sumber daya). Namun untuk kolektor bersamaan, mungkin tidak aman untuk segera beralih dari fase penandaan ke fase pembersihan. Jika thread aplikasi memodifikasi bagian memori yang telah dilacak dan ditandai oleh pengumpul sampah, referensi baru yang tidak ditandai dapat dihasilkan. Dalam beberapa implementasi pengumpulan serentak, hal ini dapat menyebabkan aplikasi terjebak dalam lingkaran anotasi berulang dalam waktu lama tanpa bisa mendapatkan memori bebas saat aplikasi membutuhkan memori ini.
Dari pembahasan sejauh ini kita mengetahui bahwa ada banyak algoritma pengumpul sampah dan pengumpulan sampah, masing-masing cocok untuk jenis aplikasi tertentu dan beban berbeda. Bukan hanya algoritma yang berbeda, tetapi implementasi algoritma yang berbeda. Oleh karena itu, yang terbaik adalah memahami kebutuhan aplikasi dan karakteristiknya sebelum menentukan pengumpul sampah. Selanjutnya, kami akan memperkenalkan beberapa kendala pada model memori platform Java. Kendala di sini mengacu pada beberapa asumsi yang cenderung dibuat oleh pemrogram Java dalam lingkungan produksi yang berubah secara dinamis yang membuat kinerja aplikasi menjadi lebih buruk.
Mengapa Tuning Tidak Bisa Menggantikan Pengumpulan Sampah
Kebanyakan pemrogram Java mengetahui bahwa ada banyak pilihan untuk mengoptimalkan program Java. Beberapa JVM opsional, pengumpul sampah, dan parameter penyetelan kinerja memungkinkan pengembang menghabiskan banyak waktu untuk penyetelan kinerja tanpa akhir. Hal ini menyebabkan beberapa orang menyimpulkan bahwa pengumpulan sampah itu buruk, dan upaya untuk membuat pengumpulan sampah lebih jarang atau berlangsung lebih singkat adalah solusi yang baik, namun hal ini berisiko.
Pertimbangkan penyetelan untuk aplikasi tertentu. Sebagian besar parameter penyetelan (seperti tingkat alokasi memori, ukuran objek, waktu respons) didasarkan pada tingkat alokasi memori aplikasi (Catatan Penerjemah: atau parameter lainnya) berdasarkan volume data pengujian saat ini. Hal ini pada akhirnya dapat menghasilkan dua hasil berikut:
1. Kasus penggunaan yang lolos pengujian gagal dalam produksi.
2. Perubahan volume data atau perubahan aplikasi memerlukan penyetelan ulang.
Penyetelan bersifat berulang, dan khususnya pengumpul sampah secara bersamaan mungkin memerlukan banyak penyetelan (terutama di lingkungan produksi). Heuristik diperlukan untuk memenuhi kebutuhan aplikasi. Untuk menghadapi skenario terburuk, hasil penyetelan mungkin berupa konfigurasi yang sangat kaku, yang juga menyebabkan banyak pemborosan sumber daya. Pendekatan penyetelan ini adalah sebuah pencarian yang aneh. Faktanya, semakin Anda mengoptimalkan pengumpul sampah agar sesuai dengan beban tertentu, semakin jauh Anda dari sifat dinamis runtime Java. Lagi pula, berapa banyak aplikasi yang memiliki beban stabil, dan seberapa andal beban yang Anda harapkan?
Jadi jika Anda tidak berfokus pada penyetelan, apa yang dapat Anda lakukan untuk mencegah kesalahan kehabisan memori dan meningkatkan waktu respons? Hal pertama adalah mencari faktor utama yang mempengaruhi kinerja aplikasi Java.
fragmentasi
Faktor yang mempengaruhi kinerja aplikasi Java bukanlah pengumpul sampah, melainkan fragmentasi dan cara pengumpul sampah menangani fragmentasi. Yang disebut fragmentasi adalah keadaan di mana ada ruang kosong di ruang heap, namun tidak ada ruang memori berdekatan yang cukup besar untuk mengalokasikan memori untuk objek baru. Seperti disebutkan dalam artikel pertama, fragmentasi memori adalah TLAB ruang yang tersisa di heap, atau ruang yang ditempati oleh objek kecil yang dilepaskan di antara objek yang berumur panjang.
Seiring berjalannya waktu dan saat aplikasi berjalan, fragmentasi ini menyebar ke seluruh heap. Dalam beberapa kasus, menggunakan parameter yang disetel secara statis bisa menjadi lebih buruk karena gagal memenuhi kebutuhan dinamis aplikasi. Aplikasi tidak dapat memanfaatkan ruang terfragmentasi ini secara efisien. Kegagalan untuk melakukan apa pun akan mengakibatkan pengumpulan sampah berturut-turut di mana pengumpul sampah mencoba mengosongkan memori untuk dialokasikan ke objek baru. Dalam kasus terburuk, bahkan pengumpulan sampah berturut-turut tidak dapat mengosongkan lebih banyak memori (terlalu banyak fragmentasi), dan kemudian JVM harus melakukan kesalahan kelebihan memori. Anda dapat mengatasi fragmentasi dengan memulai ulang aplikasi sehingga heap Java memiliki ruang memori yang berdekatan untuk mengalokasikan objek baru. Memulai ulang program akan menyebabkan waktu henti, dan setelah beberapa saat tumpukan Java akan penuh dengan fragmen lagi, sehingga memaksa restart lagi.
Kesalahan kehabisan memori yang membuat proses terhenti dan log yang menunjukkan bahwa pengumpul sampah kelebihan beban menunjukkan bahwa pengumpulan sampah mencoba mengosongkan memori dan tumpukan sangat terfragmentasi. Beberapa programmer akan mencoba memecahkan masalah fragmentasi dengan mengoptimalkan kembali pengumpul sampah. Namun menurut saya kita harus menemukan cara yang lebih inovatif untuk memecahkan masalah ini. Bagian berikut akan berfokus pada dua solusi terhadap fragmentasi: pengumpulan dan pemadatan sampah secara generasi.
Pengumpulan sampah generasi
Anda mungkin pernah mendengar teori bahwa sebagian besar objek dalam lingkungan produksi berumur pendek. Pengumpulan sampah generasi merupakan strategi pengumpulan sampah yang diturunkan dari teori ini. Dalam pengumpulan sampah generasi, kami membagi tumpukan ke dalam ruang (atau generasi) yang berbeda, dan setiap ruang menyimpan objek dengan usia berbeda. Yang disebut usia suatu objek adalah jumlah siklus pengumpulan sampah yang bertahan dari objek tersebut (yaitu, , berapa umur objek tersebut).
Bila tidak ada sisa ruang pada generasi baru untuk dialokasikan, maka benda aktif pada generasi baru akan dipindahkan ke generasi lama (biasanya hanya ada dua generasi. Catatan Penerjemah: Hanya benda yang memenuhi umur tertentu yang akan dipindahkan ke generasi lama). Pengumpulan sampah generasi sering kali menggunakan pengumpul salinan satu arah. Beberapa JVM yang lebih modern menggunakan pengumpul paralel pada generasi baru. Tentu saja, algoritme pengumpulan sampah yang berbeda dapat diterapkan untuk generasi baru dan generasi lama. Jika Anda menggunakan kolektor paralel atau kolektor penyalin, kolektor muda Anda adalah kolektor stop-the-world (lihat penjelasan sebelumnya).
Generasi lama dialokasikan pada objek yang telah dipindahkan dari generasi baru. Objek tersebut telah direferensikan sejak lama atau direferensikan oleh beberapa kumpulan objek pada generasi baru. Kadang-kadang benda berukuran besar dialokasikan langsung ke generasi lama karena biaya pemindahan benda berukuran besar relatif tinggi.
Teknologi pengumpulan sampah generasi
Pada pengumpulan sampah generasi, pengumpulan sampah lebih jarang dilakukan pada generasi lama dan lebih sering pada generasi baru, dan kami juga berharap siklus pengumpulan sampah pada generasi baru akan lebih pendek. Dalam kasus yang jarang terjadi, generasi muda mungkin lebih sering dikumpulkan dibandingkan generasi tua. Hal ini dapat terjadi jika Anda membuat generasi muda terlalu besar dan sebagian besar objek dalam aplikasi Anda berumur panjang. Dalam hal ini, jika generasi lama dibuat terlalu kecil untuk menampung seluruh benda berumur panjang, maka pengumpulan sampah generasi lama juga akan kesulitan untuk memberikan ruang bagi benda-benda yang dipindahkan. Namun, secara umum, pengumpulan sampah generasi dapat memungkinkan aplikasi mencapai kinerja yang lebih baik.
Manfaat lain dari membagi generasi baru adalah memecahkan masalah fragmentasi sampai batas tertentu, atau menunda skenario terburuk. Benda-benda kecil dengan waktu bertahan hidup yang singkat mungkin telah menyebabkan masalah fragmentasi, namun semuanya dibersihkan dalam pengumpulan sampah generasi baru. Karena objek berumur panjang mendapat ruang yang lebih kompak saat dipindahkan ke generasi lama, maka generasi lama juga lebih kompak. Seiring waktu (jika aplikasi Anda berjalan cukup lama), generasi lama juga akan menjadi terfragmentasi, memerlukan satu atau lebih pengumpulan sampah penuh untuk dijalankan, dan JVM juga dapat memunculkan kesalahan kehabisan memori. Namun menciptakan generasi baru akan menunda skenario terburuk, yang cukup untuk banyak penerapan. Untuk sebagian besar aplikasi, hal ini mengurangi frekuensi pengumpulan sampah yang terhenti dan kemungkinan kesalahan kehabisan memori.
Mengoptimalkan pengumpulan sampah generasi
Seperti disebutkan sebelumnya, penggunaan pengumpulan sampah generasi memerlukan penyesuaian yang berulang-ulang, seperti penyesuaian jumlah generasi muda, tingkat promosi, dll. Saya tidak bisa menekankan trade-off untuk runtime aplikasi tertentu: memilih ukuran tetap akan mengoptimalkan aplikasi, namun juga mengurangi kemampuan pengumpul sampah untuk mengatasi perubahan dinamis, yang tidak dapat dihindari.
Prinsip pertama dari generasi baru ini adalah meningkatkannya sebanyak mungkin sambil memastikan waktu tunda selama pengumpulan sampah, dan pada saat yang sama, menyediakan ruang yang cukup di tumpukan untuk benda-benda yang bertahan dalam jangka panjang. Berikut beberapa faktor tambahan yang perlu dipertimbangkan saat menyetel pengumpul sampah generasi:
1. Sebagian besar generasi baru adalah pemulung sampah dunia. Semakin besar pengaturan generasi baru, semakin lama waktu jedanya. Oleh karena itu, untuk aplikasi yang sangat terpengaruh oleh waktu jeda pengumpulan sampah, pertimbangkan baik-baik seberapa besar generasi mudanya.
2. Algoritme pengumpulan sampah yang berbeda dapat digunakan pada generasi yang berbeda. Misalnya pengumpulan sampah paralel digunakan pada generasi muda dan pengumpulan sampah bersamaan digunakan pada generasi tua.
3. Bila ditemukan promosi yang sering dilakukan (Catatan Penerjemah: berpindah dari generasi baru ke generasi lama) gagal, berarti terlalu banyak fragmen di generasi lama, artinya tidak ada cukup ruang di generasi lama. untuk menyimpan benda-benda yang dipindahkan dari generasi baru. Pada titik ini Anda dapat menyesuaikan tingkat promosi (yaitu menyesuaikan usia promosi), atau memastikan bahwa algoritma pengumpulan sampah di generasi lama melakukan kompresi (dibahas di paragraf berikutnya) dan menyesuaikan kompresi agar sesuai dengan beban aplikasi . Dimungkinkan juga untuk meningkatkan ukuran heap dan ukuran setiap generasi, namun hal ini akan semakin memperpanjang waktu jeda pada generasi lama. Ketahuilah bahwa fragmentasi tidak bisa dihindari.
4. Pengumpulan sampah generasi paling cocok untuk aplikasi seperti itu. Mereka memiliki banyak benda kecil dengan waktu bertahan hidup yang singkat. Banyak benda yang didaur ulang pada putaran pertama siklus pengumpulan sampah. Untuk aplikasi seperti itu, pengumpulan sampah secara berkala dapat secara efektif mengurangi fragmentasi dan menunda dampak fragmentasi.
kompresi
Meskipun pengumpulan sampah generasi menunda terjadinya kesalahan fragmentasi dan kehabisan memori, kompresi adalah satu-satunya solusi nyata untuk masalah fragmentasi. Pemadatan adalah strategi pengumpulan sampah yang melepaskan blok memori yang berdekatan dengan memindahkan objek, sehingga mengosongkan cukup ruang untuk membuat objek baru.
Memindahkan objek dan memperbarui referensi objek adalah operasi penghentian dunia yang akan membawa sejumlah konsumsi (dengan satu pengecualian, yang akan dibahas pada artikel berikutnya dalam seri ini). Semakin banyak benda yang bertahan maka semakin lama pula waktu jeda yang ditimbulkan oleh pemadatan. Dalam situasi di mana hanya ada sedikit ruang yang tersisa dan fragmentasi yang parah (biasanya karena program telah berjalan dalam waktu yang lama), mungkin ada jeda beberapa detik di area pemadatan dengan banyak objek hidup, dan ketika mendekati memori meluap, Mengompresi seluruh tumpukan bahkan bisa memakan waktu puluhan detik.
Waktu jeda pemadatan tergantung pada jumlah memori yang perlu dipindahkan dan jumlah referensi yang perlu diperbarui. Analisis statistik menunjukkan bahwa semakin besar heap, semakin besar jumlah objek hidup yang perlu dipindahkan dan referensi diperbarui. Waktu jeda adalah sekitar 1 detik untuk setiap 1 GB hingga 2 GB objek hidup yang dipindahkan, dan untuk heap ukuran 4 GB kemungkinan terdapat 25% objek hidup, jadi akan ada jeda sesekali sekitar 1 detik.
Dinding Memori Kompresi dan Aplikasi
Dinding memori aplikasi mengacu pada ukuran heap yang dapat diatur sebelum jeda yang disebabkan oleh pengumpulan sampah (misalnya: pemadatan). Tergantung pada sistem dan aplikasinya, sebagian besar dinding memori aplikasi Java berkisar antara 4 GB hingga 20 GB. Inilah sebabnya mengapa sebagian besar aplikasi perusahaan diterapkan pada beberapa JVM yang lebih kecil daripada beberapa JVM yang lebih besar. Mari kita pertimbangkan ini: Berapa banyak desain dan penerapan aplikasi Java perusahaan modern yang ditentukan oleh batasan kompresi JVM. Dalam hal ini, untuk melewati waktu jeda defragmentasi heap, kami memilih penerapan multi-instance yang lebih mahal untuk dikelola. Hal ini agak aneh mengingat besarnya kemampuan penyimpanan hardware masa kini dan perlunya peningkatan memori untuk aplikasi Java kelas enterprise. Mengapa hanya beberapa GB memori yang ditetapkan untuk setiap instance. Kompresi secara bersamaan akan meruntuhkan dinding memori, yang merupakan topik artikel saya berikutnya.
Meringkaskan
Artikel ini merupakan artikel pengantar tentang pengumpulan sampah untuk membantu Anda memahami konsep dan mekanisme pengumpulan sampah, dan semoga memotivasi Anda untuk membaca artikel terkait lebih lanjut. Banyak hal yang dibahas di sini sudah ada sejak lama, dan beberapa konsep baru akan diperkenalkan pada artikel berikutnya. Misalnya, kompresi bersamaan saat ini diterapkan oleh Zing JVM Azul. Ini adalah teknologi pengumpulan sampah baru yang mencoba mendefinisikan ulang model memori Java, terutama karena memori dan kekuatan pemrosesan terus meningkat saat ini.
Berikut beberapa poin penting tentang pengumpulan sampah yang telah saya rangkum:
1. Algoritme dan implementasi pengumpulan sampah yang berbeda beradaptasi dengan kebutuhan aplikasi yang berbeda. Pengumpul sampah pelacakan adalah pengumpul sampah yang paling umum digunakan di mesin virtual Java komersial.
2. Pengumpulan sampah paralel menggunakan seluruh sumber daya secara paralel saat melakukan pengumpulan sampah. Ini biasanya merupakan pengumpul sampah yang terhenti dan oleh karena itu memiliki throughput yang lebih tinggi, namun thread pekerja aplikasi harus menunggu hingga thread pengumpulan sampah selesai, yang berdampak tertentu pada waktu respons aplikasi.
3. Pengumpulan sampah secara bersamaan: Saat pengumpulan sedang dilakukan, thread pekerja aplikasi masih berjalan. Pengumpul sampah secara bersamaan perlu menyelesaikan pengumpulan sampah sebelum aplikasi memerlukan memori.
4. Pengumpulan sampah secara generasi membantu menunda fragmentasi, namun tidak dapat menghilangkan fragmentasi. Pengumpulan sampah generasi membagi tumpukan menjadi dua ruang, satu ruang untuk objek baru dan satu lagi untuk objek lama. Pengumpulan sampah generasi cocok untuk aplikasi dengan banyak objek kecil yang memiliki masa pakai pendek.
5. Kompresi adalah satu-satunya cara untuk mengatasi fragmentasi. Kebanyakan pengumpul sampah melakukan kompresi dengan cara stop-the-world. Semakin lama program berjalan, semakin kompleks referensi objeknya, dan semakin tidak merata distribusi ukuran objeknya, yang akan menyebabkan waktu kompresi lebih lama. Ukuran heap juga mempengaruhi waktu pemadatan, karena mungkin ada lebih banyak objek hidup dan referensi yang perlu diperbarui.
6. Penyetelan membantu menunda kesalahan kelebihan memori. Namun hasil dari over-tuning adalah konfigurasi yang kaku. Sebelum Anda mulai melakukan pendekatan coba-coba, pastikan Anda memahami beban pada lingkungan produksi, tipe objek aplikasi Anda, dan karakteristik referensi objek Anda. Konfigurasi yang terlalu kaku mungkin tidak mampu menangani beban dinamis, jadi pastikan untuk memahami konsekuensinya saat menetapkan nilai non-dinamis.
Artikel selanjutnya dalam seri ini adalah: Pembahasan mendalam tentang algoritma pengumpulan sampah C4 (Concurrent Continuously Compacting Collector), jadi pantau terus!
(Teks lengkap berakhir)