Ini adalah terjemahan dari artikel oleh Alexey Shipilev "Do It Yourself (OpenJDK) Pengumpul Sampah" , yang diterbitkan dengan persetujuan penulis. Laporkan kesalahan ketik dan bug lain di PM - kami akan memperbaikinya.
Proses menciptakan sesuatu dalam runtime adalah latihan yang menyenangkan. Setidaknya pembuatan versi pertama! Untuk membangun subsistem runtime yang andal, berkinerja tinggi, gagal-aman, perilaku yang dapat dengan mudah diamati dan didebug, adalah tugas yang sangat, sangat sulit.
Membuat pengumpul sampah sederhana itu sangat sederhana, dan sekarang saya ingin melakukan ini di artikel ini. Roman Kennke di FOSDEM 2019 membuat ceramah dan demo berjudul "Menulis GC dalam 20 Menit," menggunakan versi tambalan ini sebelumnya. Terlepas dari kenyataan bahwa kode yang diterapkan di sana menunjukkan banyak dan banyak dikomentari, ada kebutuhan untuk deskripsi tingkat tinggi yang baik tentang apa yang terjadi - ini adalah bagaimana artikel ini muncul.
Pemahaman dasar tentang pekerjaan pemulung akan sangat membantu dalam memahami apa yang ditulis di sini. Artikel ini akan menggunakan spesifik dan ide dalam implementasi spesifik dari HotSpot, tetapi tidak akan ada kursus pengantar tentang desain GC di sini. Ambil Buku Pegangan GC dan bacalah bab-bab pertama tentang dasar-dasar GC, dan bahkan lebih cepat akan memulai artikel Wikipedia .

Isi
1. Terdiri dari apa GC
Sekarang setelah banyak GC yang berbeda telah ditulis, cukup mudah untuk membuatnya sendiri - banyak elemen yang sudah ditulis dapat (kembali) digunakan untuk mengalihkan beberapa kekhawatiran tentang detail implementasi ke kode yang terbukti dan teruji.
1.1. Epsilon gc
OpenJDK 11 memperkenalkan JEP 318 baru: "Epsilon: Seorang Kolektor Sampah No-Op (Eksperimental) . " Tugasnya adalah untuk menyediakan implementasi minimal untuk kasus ketika membebaskan memori tidak diperlukan atau bahkan dilarang. JEP membahas secara lebih rinci mengapa ini mungkin berguna.
Dari sudut pandang implementasi, "pengumpul sampah" adalah nama yang buruk, akan lebih tepat untuk menggunakan istilah "manajer memori otomatis" , yang bertanggung jawab untuk mengalokasikan dan membebaskan memori. Epsilon GC hanya mengimplementasikan "alokasi", dan tidak berurusan dengan "rilis" sama sekali. Oleh karena itu, Anda dapat mengambil Epsilon GC dan mulai menerapkan algoritma "rilis" dari awal.
1.1.1. Alokasi memori
Bagian yang paling berkembang dari Epsilon GC bertanggung jawab untuk mengalokasikan memori . Ini melayani permintaan eksternal untuk mengalokasikan memori ukuran sewenang-wenang dan membuat Thread-Local Allocation Buffer (TLAB) dari ukuran yang diinginkan. Implementasi itu sendiri berusaha untuk tidak memperpanjang TLAB terlalu banyak, karena tidak akan ada memori bebas dan tidak ada yang akan mengembalikan byte yang hilang.
1.1.2. Hambatan
Beberapa pengumpul sampah memerlukan interaksi dengan aplikasi untuk mempertahankan invarian GC, memaksa runtime dan aplikasi untuk membuat hambatan yang disebut ketika mencoba mengakses heap. Ini berlaku untuk semua kolektor multi-utas, serta bagi banyak kolektor dari generasi ke generasi dan menghentikan dunia.
Epsilon tidak memerlukan penghalang, tetapi runtime dan kompiler masih ingin tahu bahwa penghalang tidak melakukan apa-apa. Penanganannya setiap saat di mana saja bisa melelahkan. Untungnya, dimulai dengan OpenJDK 11, ada JEP-304 baru: "Garbage Collection Interface" , yang membuatnya jauh, jauh lebih mudah untuk memasukkan hambatan. Secara khusus, penghalang yang ditetapkan dalam Epsilon kosong , dan semua pekerjaan sepele - save, load, CAS, arraycopy - dapat didelegasikan ke implementasi hambatan sepele dari superclass yang ada. Jika Anda membuat GC yang juga tidak memerlukan penghalang, Anda bisa menggunakan kembali kode dari Epsilon.
1.1.3. Memantau koneksi
Bagian membosankan terakhir dari implementasi GC adalah kaitan dengan sekelompok mekanisme pemantauan di dalam JVM: MX-bins, perintah diagnostik, dll. Harus bekerja. Epsilon sudah melakukan semua ini untukmu.
1.2. Rantime dan GC
1.2.1. Elemen root
Pengumpul sampah, dalam kasus umum, perlu tahu apa tepatnya di runtime Java yang memiliki referensi tumpukan. Elemen-elemen root ini, disebut root GC , dapat menjadi slot pada stream stream dan variabel lokal (termasuk yang ditemukan dalam kode yang dikompilasi JIT!), Kelas asli dan pemuat kelas, referensi di JNI, dan sebagainya. Upaya untuk mengidentifikasi elemen-elemen ini bisa sangat kompleks dan membosankan. Tetapi di Hotspot, mereka semua dilacak menggunakan subsistem VM yang sesuai, sehingga Anda bisa belajar bagaimana implementasi GC yang ada bekerja dengannya. Lebih jauh dalam teks kita akan melihatnya.
1.2.2. Penjelajahan Objek
Pengumpul sampah harus memotong tautan keluar di objek Java. Operasi ini ditemukan di mana-mana, jadi bagian umum dari runtime menyediakan solusi yang sudah jadi, Anda tidak perlu menulis apa pun sendiri. Di bawah ini akan ada bagian dengan implementasi spesifik, dan di sana Anda dapat menemukan, misalnya, memanggil obj→oop_iterate
.
1.2.3. Pemindahan
Pengumpul sampah yang bergerak perlu menuliskan alamat baru dari objek yang dipindahkan di suatu tempat. Ada beberapa tempat di mana Anda dapat menulis data penerusan ini.
- Anda dapat menggunakan kembali "kata penanda" pada objek itu sendiri (Serial, Paralel, dll.). Setelah dunia berhenti, semua akses ke objek dikendalikan, dan dijamin bahwa tidak ada utas Java yang dapat melihat data sementara yang kami putuskan untuk dimasukkan dalam kata penanda. Anda dapat menggunakannya kembali untuk menyimpan data penerusan.
- Anda dapat mempertahankan tabel gerakan asli yang terpisah ( ZGC , C4, dan lainnya). Ini sepenuhnya mengisolasi GC dari runtime dan sisa aplikasi, karena hanya GC yang tahu tentang keberadaan tabel seperti itu. Perakit yang kompetitif biasanya hanya menggunakan skema seperti itu - mereka tidak ingin menderita dengan banyak masalah yang tidak perlu.
- Anda dapat menambahkan kata lain ke objek ( Shenandoah dan lainnya). Kombinasi dari dua pendekatan sebelumnya tidak hanya memungkinkan runtime dan aplikasi untuk bekerja dengan header yang ada tanpa masalah, tetapi juga menyimpan data penerusan.
1.2.4. Data penanda
Pengumpul sampah perlu menulis data penandaan di suatu tempat. Dan lagi, ada beberapa cara untuk menyelamatkan mereka:
- Anda dapat menggunakan kembali kata penanda di objek itu sendiri (Serial, Paralel, dll.). Sekali lagi, dalam mode berhenti dunia, Anda dapat menggunakan bit dalam kata penanda untuk menyandikan fakta label. Lebih lanjut, jika Anda perlu berkeliling semua benda hidup, kita pergi bersama tumpukan, objek demi objek - ini dimungkinkan karena fakta bahwa tumpukan itu dapat diuraikan .
- Anda dapat mempertahankan struktur terpisah untuk menyimpan data penandaan (G1, Shenandoah, dll.). Ini biasanya dilakukan dengan menggunakan bitmap terpisah , yang memetakan setiap N byte dari heap ke 1 bit kartu. Biasanya, objek Java disejajarkan oleh 8 byte , sehingga kartu memetakan setiap 64 bit dari tumpukan ke 1 bit kartu, menempati 1/64 dari ukuran tumpukan di memori asli. Overhead ini terbayar dengan baik ketika memindai tumpukan untuk keberadaan benda hidup, terutama yang jarang: melewati peta seringkali jauh lebih cepat daripada memotong tumpukan yang dibongkar objek demi objek.
- Encode label menjadi tautan sendiri (ZGC, C4 dan lainnya). Ini membutuhkan koordinasi dengan aplikasi, maka Anda perlu memotong semua label ini dari tautan atau melakukan beberapa trik lain untuk menjaga kebenaran. Dengan kata lain, kita membutuhkan penghalang atau pekerjaan tambahan dari GC.
2. Rencana umum
Kemungkinan besar, yang paling mudah diterapkan di atas Epsilon adalah Mark-Compact, dengan gaya LISP2. Ide dasar dari GC ini dijelaskan baik di Wikipedia maupun dalam GC Handbook (bab 3.2). Sketsa algoritme akan ada di bagian dengan implementasi di bawah ini, tetapi saya sangat menyarankan membaca sedikit Wikipedia atau Buku Pegangan GC untuk memahami apa yang akan kita lakukan.
Algoritma yang dimaksud adalah GC penggeser : objek bergerak bergerak dalam larik ke awal tumpukan. Ini memiliki pro dan kontra:
- Ini mempertahankan urutan alokasi memori. Ini sangat baik untuk mengendalikan tata letak dalam memori, jika itu penting bagi Anda (control freaks, saatnya Anda!). Kelemahannya adalah Anda tidak akan mendapatkan lokasi tautan otomatis seperti ini.
- Kompleksitasnya adalah O (N) dari jumlah objek. Namun, linearitas ada harganya: GC diperlukan untuk mem-bypass sekelompok 4 kali untuk setiap siklus build.
- Tidak memerlukan memori bebas di heap! Tidak perlu menyimpan memori pada heap untuk mengevakuasi objek hidup, sehingga Anda bahkan dapat bekerja dengan heap yang dilampaui 99. (9)%. Jika kita mengambil ide-ide lain dari pengumpul sederhana, misalnya, seorang pemulung dengan semi-ruang (semi-space scavenger), kita harus sedikit menulis ulang presentasi tumpukan dan menyimpan sedikit ruang untuk evakuasi, tetapi ini berada di luar cakupan latihan ini.
- Jika Anda bekerja sedikit tentang masalah ini, Anda dapat mencapai nol memori dan konsumsi waktu selama periode ketika GC tidak aktif. Itu dimulai pada memori dalam keadaan acak, dan berhenti, secara signifikan memadatkannya. Ini sangat cocok dengan cara kerja Epsilon: ia hanya menyorot tepat setelah objek terakhir. Ini juga minus: beberapa benda mati di awal tumpukan menyebabkan sejumlah besar gerakan.
- Hanya saja tidak memerlukan hambatan baru, Anda dapat menggunakan kembali
EpsilonBarrierSet
apa adanya.
Untuk kesederhanaan, implementasi GC akan menggunakan perhentian penuh dunia (stop-the-world, STW), ia tidak akan memiliki generasi atau multithreading. Untuk kasus ini, masuk akal untuk menggunakan bitmap untuk menyimpan tanda dan menggunakan kembali kata penanda untuk menyimpan data pergerakan.
3. Implementasi inti GC
Membaca dan memahami keseluruhan implementasi mungkin terlalu rumit untuk orang yang tidak tahu apa-apa. Di bagian ini, kita akan memahaminya langkah demi langkah.
3.1. Prolog
Pengumpul sampah biasanya perlu melakukan beberapa hal untuk mempersiapkan pengumpulannya. Baca komentar, mereka harus berbicara sendiri:
{ GCTraceTime(Info, gc) time("Step 0: Prologue", NULL);
Karena kita menggunakan bitmap untuk melacak jangkauan objek, kita perlu menghapusnya sebelum digunakan. Atau dalam kasus kami, karena kami bertujuan untuk tidak pernah meminta sumber daya sebelum memulai siklus GC, kami harus memasukkan bitmap ke memori terlebih dahulu. Ini memberikan beberapa keuntungan menarik, setidaknya di Linux, di mana sebagian besar bitmap akan mengarah ke halaman nol, terutama untuk tumpukan yang jarang.
Utas harus membebaskan TLAB mereka dan meminta GC untuk yang baru setelah build selesai.
Jangan bingung TLAB dan java.lang.ThreadLocal
. Dari sudut pandang GC, ThreadLocal adalah objek biasa, dan mereka tidak akan dikompilasi oleh GC kecuali secara khusus diminta sebaliknya dalam kode Java.
Beberapa bagian runtime, terutama yang menahan tautan ke Java heap, akan rusak saat pengumpulan sampah, jadi Anda perlu memperingatkan mereka bahwa GC akan segera mulai bekerja. Ini akan memungkinkan masing-masing subsistem untuk menyiapkan dan menyimpan bagian dari negara mereka sebelum GC mengambil langkah.
3.2. Menandai
Menandai dalam mode berhenti dunia menjadi sangat sederhana ketika hampir semuanya telah dilakukan untuk kita. Pelabelan cukup standar, dan kemungkinan besar, dalam banyak implementasi, GC adalah langkah pertama.
{ GCTraceTime(Info, gc) time("Step 1: Mark", NULL);
Ini berfungsi persis sama dengan untuk grafik lain: Anda memulai traversal dengan set awal dari simpul yang dapat dijangkau, berjalan di sepanjang tepi keluar dan merekam semua simpul yang dikunjungi. Traversal berlanjut hingga semua puncak yang belum dikunjungi berakhir. Dalam GC, "simpul" adalah objek, dan "ujung" adalah tautan di antara mereka.
Secara teknis, kita bisa secara rekursif menelusuri grafik objek, tetapi ini adalah ide yang buruk untuk grafik arbitrer yang dapat memiliki diameter sangat besar. Bayangkan daftar tertaut dari satu miliar puncak! Oleh karena itu, untuk membatasi kedalaman rekursi, kami menggunakan tumpukan penandaan yang merekam objek yang terdeteksi.
Set awal objek yang dapat dijangkau adalah akar GC. Sekarang jangan memikirkan apa itu process_roots
, lebih lanjut tentang itu nanti. Untuk saat ini, anggap saja ia mem-bypass semua tautan yang dapat dijangkau dari sisi VM.
Bitmap dengan tanda berfungsi baik sebagai alat untuk merekam muka gelombang penandaan (banyak objek sudah dikunjungi), dan pada akhirnya - sebagai repositori dari hasil yang diinginkan, satu set semua objek yang dapat dijangkau. Pekerjaan nyata berlangsung di EpsilonScanOopClosure
, ini diterapkan ke semua objek yang menarik dan diulangi di semua tautan dari objek yang dipilih.
Lihat, Jawa tahu cara menutup (closure) sebelum menjadi modis!
class EpsilonScanOopClosure : public BasicOopIterateClosure { private: EpsilonMarkStack* const _stack; MarkBitMap* const _bitmap; template <class T> void do_oop_work(T* p) {
Setelah menyelesaikan langkah ini, _bitmap
berisi bit yang menunjukkan lokasi objek langsung. Berkat ini, dimungkinkan untuk mem-bypass semua benda hidup, misalnya:
3.3. Hitung alamat baru
Ini juga merupakan langkah yang cukup sederhana, dan mengimplementasikan persis apa yang dikatakan algoritma.

Satu-satunya hal yang menarik perhatian Anda adalah bahwa kami memutuskan untuk menyimpan alamat baru di kata menandai objek Java, dan kata ini sudah dapat ditempati oleh sesuatu yang penting, misalnya, informasi tentang kunci. Untungnya, kata-kata yang menandai nontrivial semacam itu cukup langka, dan kita dapat menyimpannya secara terpisah, jika memang diperlukan: inilah yang digunakan untuk PreservedMarks
.
Pekerjaan algoritmik nyata dilakukan oleh EpsilonCalcNewLocationObjectClosure
:
class EpsilonCalcNewLocationObjectClosure : public ObjectClosure { private: HeapWord* _compact_point; PreservedMarks* const _preserved_marks; public: EpsilonCalcNewLocationObjectClosure(HeapWord* start, PreservedMarks* pm) : _compact_point(start), _preserved_marks(pm) {} void do_object(oop obj) {
forward_to
adalah bagian terpenting karena ia menyimpan "pindahkan alamat" pada kata marker objek. Ini akan diperlukan pada langkah selanjutnya.
3.4. Perbaiki pointer
Sekarang Anda harus melalui tumpukan lagi dan menulis ulang semua tautan dengan alamat baru mereka sesuai dengan algoritma berikut:

{ GCTraceTime(Info, gc) time("Step 3: Adjust pointers", NULL);
Ada dua jenis referensi untuk objek bergeser: keluar baik dari objek di heap itu sendiri, atau dari akar GC. Anda perlu memperbarui kedua kelas tautan. Beberapa label yang disimpan juga menyimpan referensi ke objek, jadi Anda perlu meminta mereka untuk memperbarui. PreservedMarks
tahu bagaimana melakukan ini karena mengharapkan "meneruskan data" di tempat yang sama tempat kita menyimpannya, dalam kata penandaan objek.
Penutupan dibagi menjadi dua jenis: beberapa mengambil objek dan memotong isinya, yang lain memperbarui alamat ini. Di sini Anda dapat membuat optimasi kinerja kecil: jika objek tidak bergerak, Anda dapat menyimpan beberapa catatan dalam banyak:
class EpsilonAdjustPointersOopClosure : public BasicOopIterateClosure { private: template <class T> void do_oop_work(T* p) {
Setelah menyelesaikan langkah ini, kami pada dasarnya memecah tumpukan: tautan menunjuk ke alamat "salah" di mana objek belum berbohong. Mari kita perbaiki!
3.5. Kami memindahkan objek
Saatnya memindahkan objek ke alamat baru, sesuai dengan algoritma:

EpsilonMoveObjectsObjectClosure
berkeliling tumpukan lagi dan menerapkan penutupan EpsilonMoveObjectsObjectClosure
untuk semua objek hidup:
{ GCTraceTime(Info, gc) time("Step 4: Move objects", NULL);
Segera setelah itu, Anda dapat menarik tumpukan tumpukan titik pemadatan, memungkinkan untuk mengalokasikan memori langsung dari tempat ini, segera setelah siklus pengumpulan sampah berakhir.
Perhatikan bahwa dalam rakitan pemindahan kita dapat menimpa konten objek yang ada, tetapi karena pemindaian berjalan ke arah yang sama, objek yang ditimpa telah disalin ke tempat yang tepat.
Lokasi lama dan baru dari fasilitas yang sama dapat berpotongan. Misalnya, jika Anda menggeser objek 100-byte dengan 8 byte. Prosedur penyalinan harus berhasil dengan sendirinya, dan konten yang berpotongan harus disalin dengan benar, perhatikan Copy::aligned_*conjoint*_words
.
Penutupan itu sendiri hanya akan memindahkan objek yang dipindahkan ke alamat baru:
class EpsilonMoveObjectsObjectClosure : public ObjectClosure { public: void do_object(oop obj) {
3.6. Epilog
Pengumpulan sampah selesai, tumpukan sekali lagi hampir konsisten, sentuhan terakhir yang tersisa:
{ GCTraceTime(Info, gc) time("Step 5: Epilogue", NULL);
Kami memberi tahu sisa runtime bahwa mereka harus memulai prosedur pasca-perakitan. Kami mengembalikan kata-kata penanda khusus yang kami simpan sebelumnya. Ciuman perpisahan untuk kartu penanda kami - itu tidak lagi diperlukan.
Dan, jika Anda benar-benar ingin, Anda dapat mengurangi memori untuk alokasi ke ukuran baru, sehingga mengembalikan memori ke sistem operasi!
4. Hubungkan GC ke VM
4.1. Root Traversal
Ingat, Anda perlu mem-bypass tautan khusus yang dapat dijangkau dari VM? Anda dapat meminta setiap subsistem VM khusus untuk memotong tautan yang tersembunyi dari objek Java lainnya. Daftar lengkap elemen-elemen root tersebut di Hotspot saat ini terlihat seperti ini:
void EpsilonHeap::do_roots(OopClosure* cl) {
, . GC .
4.2.
GC , VM . Hotspot VM_Operation
, GC VM- :
, GC — , .
4.3.
, GC , , GC , . , allocate_work
, GC :
HeapWord* EpsilonHeap::allocate_or_collect_work(size_t size) { HeapWord* res = allocate_work(size); if (res == NULL && EpsilonSlidingGC) { vmentry_collect(GCCause::_allocation_failure); res = allocate_work(size); } return res; }
!
5.
OpenJDK.
$ hg clone https://hg.openjdk.java.net/jdk/jdk/ jdk-jdk $ cd jdk-jdk $ curl https://shipilev.net/jvm/diy-gc/webrev/jdk-jdk-epsilon.changeset | patch -p1
OpenJDK :
$ ./configure --with-debug-level=fastdebug $ make images
:
$ build/linux-x86_64-server-fastdebug/images/jdk/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC -version openjdk version "13-internal" 2019-09-17 OpenJDK Runtime Environment (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon) OpenJDK 64-Bit Server VM (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon, mixed mode, sharing
6.
, GC ? :
- . . Hotspot , JVM fastdebug , GC.
- . , . , ( ) , .
- Tes. , , , . - , .
, , :
$ CONF=linux-x86_64-server-fastdebug make images run-test TEST=gc/epsilon/ Building targets 'images run-test' in configuration 'linux-x86_64-server-fastdebug' Test selection 'gc/epsilon/', will run: * jtreg:test/hotspot/jtreg/gc/epsilon Running test 'jtreg:test/hotspot/jtreg/gc/epsilon' Passed: gc/epsilon/TestAlwaysPretouch.java Passed: gc/epsilon/TestAlignment.java Passed: gc/epsilon/TestElasticTLAB.java Passed: gc/epsilon/TestEpsilonEnabled.java Passed: gc/epsilon/TestHelloWorld.java Passed: gc/epsilon/TestLogTrace.java Passed: gc/epsilon/TestDieDefault.java Passed: gc/epsilon/TestDieWithOnError.java Passed: gc/epsilon/TestMemoryPools.java Passed: gc/epsilon/TestMaxTLAB.java Passed: gc/epsilon/TestPrintHeapSteps.java Passed: gc/epsilon/TestArraycopyCheckcast.java Passed: gc/epsilon/TestClasses.java Passed: gc/epsilon/TestUpdateCountersSteps.java Passed: gc/epsilon/TestDieWithHeapDump.java Passed: gc/epsilon/TestByteArrays.java Passed: gc/epsilon/TestManyThreads.java Passed: gc/epsilon/TestRefArrays.java Passed: gc/epsilon/TestObjects.java Passed: gc/epsilon/TestElasticTLABDecay.java Passed: gc/epsilon/TestSlidingGC.java Test results: passed: 21 TEST SUCCESS
? fastdebug . ? - .
7.
- spring-petclinic , Apache Bench GC! , , GC .
: -Xlog:gc -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC
:
:
Heap: 20480M reserved, 20480M (100.00%) committed, 19497M (95.20%) used GC(2) Step 0: Prologue 2.085ms GC(2) Step 1: Mark 51.005ms GC(2) Step 2: Calculate new locations 71.207ms GC(2) Step 3: Adjust pointers 49.671ms GC(2) Step 4: Move objects 22.839ms GC(2) Step 5: Epilogue 1.008ms GC(2) GC Stats: 70561 (8.63%) reachable from roots, 746676 (91.37%) reachable from heap, 91055 (11.14%) moved, 2237 (0.27%) markwords preserved GC(2) Heap: 20480M reserved, 20480M (100.00%) committed, 37056K (0.18%) used GC(2) Lisp2-style Mark-Compact (Allocation Failure) 20479M->36M(20480M) 197.940ms
200 ? GC! , . , , : ( — , ). - ( ).
, GC . , -Xlog:gc -XX:+UseSerialGC
— , , :
GC(46) Pause Young (Allocation Failure) 575M->39M(1943M) 2.603ms GC(47) Pause Young (Allocation Failure) 575M->39M(1943M) 2.606ms GC(48) Pause Young (Allocation Failure) 575M->39M(1943M) 2.747ms GC(49) Pause Young (Allocation Failure) 575M->39M(1943M) 2.578ms
, 2 . , , GC . -Xlog:gc -XX:+UseSerialGC
, , :
GC(3) Pause Full (Allocation Failure) 16385M->34M(18432M) 1969.694ms GC(4) Pause Full (Allocation Failure) 16385M->34M(18432M) 2261.405ms GC(5) Pause Full (Allocation Failure) 16385M->34M(18432M) 2327.577ms GC(6) Pause Full (Allocation Failure) 16385M->34M(18432M) 2328.976ms
, . .
8. ?
. , GC OpenJDK — , , .
:
. , // . . , , « » , , .
GC, java.lang.ref.Reference.referent
— Java-, , , - . FinalReference
, .
ReferenceProcessor
/ / .
VM. VM, , , . . , , , - , .
. — , GC, . , , , .
mark-compact GC Full GC fallbacks Shenandoah ( OpenJDK 8) G1 ( OpenJDK 10, JEP 307: «Parallel Full GC for G1» ).
. , «» , , , - . . , .
. , , , . , «» — «» «» , .
- GC Handbook .
9.
? GC — , , , GC.
, - GC . , , GC (, Serial GC Parallel GC), .
Menit periklanan. , 5-6 2019, JPoint — Java-. — OpenJDK, GraalVM, Kotlin . .