Dalam serangkaian artikel, saya ingin menyangkal kesalahpahaman yang terkait dengan manajemen memori dan melihat lebih dalam strukturnya dalam beberapa bahasa pemrograman modern - Java, Kotlin, Scala, Groovy, dan Clojure. Semoga artikel ini akan membantu Anda mengetahui apa yang terjadi di balik terpal bahasa-bahasa ini. Pertama, kita akan melihat manajemen memori di Java Virtual Machine (JVM) , yang digunakan di Java, Kotlin, Scala, Clojure, Groovy, dan bahasa lainnya. Di artikel pertama, saya juga membahas perbedaan antara tumpukan dan heap, yang berguna untuk memahami artikel ini.
Struktur memori JVM
Pertama, mari kita lihat struktur memori JVM. Struktur ini telah digunakan sejak JDK 11 . Ini adalah memori yang tersedia untuk proses JVM, ini dialokasikan oleh sistem operasi:
Ini adalah memori asli yang dialokasikan oleh OS dan ukurannya bergantung pada sistem, prosesor, dan JRE. Area apa dan apa yang dimaksudkan untuk itu?
Tumpukan
Di sinilah JVM menyimpan objek dan data dinamis. Ini adalah area memori terbesar dan tempat pengumpul sampah bekerja. Ukuran heap dapat dikontrol dengan flag
Xms
(ukuran awal) dan
Xmx
(ukuran maksimum). Heap tidak ditransfer ke mesin virtual secara keseluruhan, beberapa bagian dicadangkan sebagai ruang virtual, sehingga heap dapat bertambah di masa mendatang. Heap dibagi menjadi ruang generasi "muda" dan "tua".
- Generasi muda , atau "ruang baru": wilayah tempat benda-benda baru hidup. Ini dibagi menjadi Eden Space dan Survivor Space. Daerah muda generasi kontrol, " muda sampah kolektor » (Kecil GC), yang juga disebut "muda» (Young GC) tersebut.
- Paradise : Di sinilah memori dialokasikan saat kita membuat objek baru.
- Area penyintas : Di sinilah benda-benda yang tersisa dari pengumpul sampah minor disimpan. Area tersebut dibagi menjadi dua bagian, S0 dan S1 .
- Generasi lama , atau "penyimpanan" (Tenured Space): Ini mencakup objek yang telah mencapai ambang penyimpanan maksimum selama masa pakai pengumpul sampah junior. Ruang ini dikelola oleh Major GC.
Tumpukan benang
Ini adalah area tumpukan di mana satu tumpukan dialokasikan per utas. Di sinilah data statis khusus utas disimpan, termasuk bingkai metode dan fungsi, dan penunjuk ke objek. Ukuran memori stack dapat diatur menggunakan sebuah bendera
Xss
.
Metaspace
Ini adalah bagian dari memori native, secara default tidak memiliki batas atas. Dalam versi JVM sebelumnya, memori ini disebut ruang generasi permanen (Permanent Generation (PermGen) Space) . Pemuat kelas menyimpan definisi kelas di dalamnya. Jika ruang ini bertambah, maka OS dapat memindahkan data yang disimpan di sini dari RAM ke memori virtual, yang dapat memperlambat aplikasi. Ini dapat dihindari dengan mengatur ukuran MetaSpace melalui flag
XX:MetaspaceSize
dan
-XX:MaxMetaspaceSize
, dalam hal ini, aplikasi dapat mengeluarkan kesalahan memori.
Cache kode
Di sinilah kompilator Just In Time (JIT) menyimpan blok kode terkompilasi yang perlu sering Anda akses. Biasanya JVM menafsirkan bytecode menjadi kode mesin asli, namun kode yang dikompilasi oleh kompiler JIT tidak perlu diinterpretasikan, itu sudah dalam format asli dan di-cache di area memori ini.
Perpustakaan Bersama
Di sinilah kode asli untuk setiap perpustakaan bersama disimpan. Area memori ini dimuat oleh sistem operasi hanya sekali untuk setiap proses.
Penggunaan memori JVM: stack dan heap
Sekarang mari kita lihat bagaimana program yang dapat dieksekusi menggunakan bagian paling penting dari memori. Mari gunakan kode di bawah ini. Ini tidak dioptimalkan untuk ketepatan, jadi abaikan masalah seperti variabel perantara yang tidak perlu, pengubah salah, dan banyak lagi. Tugasnya adalah memvisualisasikan penggunaan tumpukan dan heap.
class Employee {
String name;
Integer salary;
Integer sales;
Integer bonus;
public Employee(String name, Integer salary, Integer sales) {
this.name = name;
this.salary = salary;
this.sales = sales;
}
}
public class Test {
static int BONUS_PERCENTAGE = 10;
static int getBonusPercentage(int salary) {
int percentage = salary * BONUS_PERCENTAGE / 100;
return percentage;
}
static int findEmployeeBonus(int salary, int noOfSales) {
int bonusPercentage = getBonusPercentage(salary);
int bonus = bonusPercentage * noOfSales;
return bonus;
}
public static void main(String[] args) {
Employee john = new Employee("John", 5000, 5);
john.bonus = findEmployeeBonus(john.salary, john.sales);
System.out.println(john.bonus);
}
}
Di sini Anda dapat melihat bagaimana program di atas dijalankan dan bagaimana stack dan heap digunakan:
https://files.speakerdeck.com/presentations/9780d352c95f4361 E5E5E5c6fa164554afc / JVM_memory_use.pdf
Seperti yang Anda lihat:
- Setiap panggilan fungsi didorong ke thread tumpukan eksekusi sebagai blok bingkai.
- Semua variabel lokal, termasuk argumen dan nilai yang dikembalikan, disimpan di tumpukan di dalam blok bingkai fungsi.
- int .
- Employee, Integer String , . .
- , , .
- , .
- ().
- , .
Tumpukan tersebut secara otomatis dikelola oleh sistem operasi, bukan JVM. Karena itu, tidak perlu merawatnya secara khusus. Namun heap tidak lagi dikelola dengan cara ini, dan karena ini adalah area memori terbesar yang berisi data dinamis, heap dapat bertambah secara eksponensial, dan program dapat menggunakan semua memori dari waktu ke waktu. Selain itu, heap secara bertahap menjadi terfragmentasi, sehingga memperlambat kinerja aplikasi. JVM akan membantu menyelesaikan masalah ini. Ini secara otomatis mengelola heap menggunakan pengumpulan sampah.
Manajemen memori JVM: pengumpulan sampah
Mari kita lihat manajemen heap otomatis, yang memainkan peran sangat penting dalam kinerja aplikasi. Ketika sebuah program mencoba mengalokasikan lebih banyak memori pada heap daripada yang tersedia (tergantung pada nilainya
Xmx
), kita keluar dari kesalahan memori .
JVM mengelola heap menggunakan pengumpulan sampah. Untuk memberi ruang bagi pembuatan objek baru, JVM membersihkan memori yang ditempati oleh objek yatim piatu, yaitu objek yang tidak lagi direferensikan secara langsung atau tidak langsung dari tumpukan.
Pengumpul sampah JVM bertanggung jawab untuk:
- Mendapatkan memori dari OS dan mengembalikannya ke OS.
- Transfer memori yang dialokasikan ke aplikasi atas permintaannya.
- Tentukan bagian mana dari memori yang dialokasikan yang masih digunakan oleh aplikasi.
- Mengklaim memori yang tidak terpakai untuk digunakan oleh aplikasi.
Pengumpul sampah di JVM bekerja berdasarkan generasi (objek di heap dikelompokkan berdasarkan usia dan dibersihkan selama tahapan yang berbeda). Ada banyak algoritma pengumpulan sampah yang berbeda, tetapi Mark & Sweep adalah yang paling umum digunakan .
Mark & Sapu Pengumpul Sampah
JVM menggunakan utas daemon terpisah yang berjalan di latar belakang untuk pengumpulan sampah. Proses ini dimulai ketika kondisi tertentu terpenuhi. Kolektor Mark & Sweep biasanya bekerja dalam dua tahap, terkadang tahap ketiga ditambahkan, tergantung pada algoritme yang digunakan.
- Markup : Pertama, kolektor menentukan objek mana yang digunakan dan yang tidak. Yang digunakan atau diakses oleh penunjuk tumpukan secara rekursif ditandai sebagai hidup.
- Penghapusan : Kolektor berjalan melalui heap dan menghapus semua objek yang tidak ditandai hidup. Lokasi memori ini ditandai sebagai kosong.
- Kompresi : Setelah menghapus objek yang tidak digunakan, semua objek yang masih hidup dipindahkan sehingga menjadi satu. Ini mengurangi fragmentasi dan mempercepat alokasi memori untuk objek baru.
Jenis kolektor ini juga disebut stop-the-world, karena saat dihapus, ada jeda dalam aplikasi.
JVM menawarkan beberapa algoritme pengumpulan sampah yang berbeda untuk dipilih, dan bergantung pada JDK Anda, mungkin ada lebih banyak opsi (misalnya, pengumpul Shenandoah di OpenJDK). Penulis implementasi yang berbeda bertujuan pada tujuan yang berbeda:
- Throughput : Waktu yang dihabiskan untuk pengumpulan sampah, bukan menjalankan aplikasi. Idealnya, throughput harus tinggi, yaitu jeda pengumpulan sampah yang singkat.
- Durasi jeda : Berapa lama pengumpul sampah mengganggu eksekusi aplikasi. Idealnya, jeda harus sangat singkat.
- Ukuran heap : Idealnya harus kecil.
Kolektor di JDK 11
JDK 11 adalah rilis LTE saat ini. Di bawah ini adalah daftar pengumpul sampah yang tersedia di dalamnya, dan JVM memilihnya secara default bergantung pada perangkat keras dan sistem operasi saat ini. Kami selalu dapat memaksa pemilih untuk dipilih menggunakan tombol radio
-XX
.
- : , , .
-XX:+UseSerialGC
. - : , . , / .
-XX:+UseParallelGC
. - Garbage-First (G1): ( ). , . .
-XX:+UseG1GC
. - Z: , , JDK11. . , stop-the-world. , / ( ).
-XX:+UseZGC
.
Terlepas dari kolektor mana yang dipilih, JVM menggunakan dua jenis perakitan - kolektor junior dan kolektor senior.
Perakit Junior
Ini menjaga kebersihan dan kekompakan ruang generasi muda. Ini diluncurkan ketika JVM tidak bisa mendapatkan memori yang diperlukan di surga untuk menampung objek baru. Awalnya, semua area heap kosong. Surga mengisi pertama, diikuti oleh area para penyintas, dan di akhir penyimpanan.
Anda dapat melihat proses pengumpul ini di sini:
https://files.speakerdeck.com/presentations/f4783404769145f4b990154d0cc05629/JVM_minor_GC.pdf
- Misalkan sudah ada objek di surga (blok 01 sampai 06 ditandai sebagai digunakan).
- Aplikasi membuat objek baru (07).
- JVM , , JVM .
- ( ), — ().
- JVM S0 S1 «» (To Space), S0. «» , , , .
- , .
- , - , ( 07 13 ).
- (14).
- JVM , , JVM .
- , , « ».
- JVM «» S1, S0 «». «» «» (S1), , . , «», , (premature promotion). , .
- «» (S0), .
- Ini diulangi dengan setiap sesi kolektor junior, korban pindah antara S0 dan S1, dan usia mereka bertambah. Ketika mencapai "ambang batas maksimum" yang ditentukan, yaitu 15 secara default, objek dipindahkan ke "penyimpanan".
Kami melihat bagaimana kolektor junior membersihkan memori di ruang generasi muda. Ini adalah proses stop-the-world, tetapi sangat cepat sehingga durasinya biasanya dapat diabaikan.
Perakit Senior
Memantau kebersihan dan kekompakan ruang (penyimpanan) generasi lama. Berjalan di bawah salah satu kondisi berikut:
- Pengembang memanggil program tersebut
System
.gc()
atauRuntime.getRunTime().gc()
. - JVM memutuskan bahwa penyimpanan kehabisan memori karena penuh sebagai hasil sesi kolektor junior sebelumnya.
- Jika saat menjalankan JVM kolektor junior tidak bisa mendapatkan cukup memori di surga atau daerah penyintas.
- Jika kita menetapkan parameter di JVM
MaxMetaspaceSize
dan tidak ada cukup memori untuk memuat kelas baru.
Proses kerja kolektor senior lebih sederhana daripada yang junior:
- Katakanlah banyak sesi kolektor junior telah berlalu dan penyimpanan hampir penuh. JVM memutuskan untuk menjalankan kolektor lama.
- Dalam penyimpanan, secara rekursif melintasi grafik objek mulai dari penunjuk tumpukan dan menandai objek yang digunakan sebagai (memori bekas), sisanya sebagai sampah (hilang). Jika kolektor senior diluncurkan selama karya kolektor junior, maka karyanya mencakup ruang generasi muda (surga dan area penyintas) dan lemari besi.
- Kolektor menghapus semua benda yatim piatu dan mengambil kembali memori.
- Jika tidak ada objek yang tersisa di heap selama kolektor lama bekerja, JVM juga mengambil kembali memori dari metaspace, menghapus kelas yang dimuat darinya, jika ini adalah kumpulan sampah lengkap.
Kesimpulan
Kami telah membahas struktur dan manajemen memori JVM. Ini bukan artikel lengkap, kami belum membicarakan banyak konsep yang lebih kompleks dan cara menyesuaikan untuk kasus penggunaan tertentu. Anda dapat membaca lebih detail di sini .
Tetapi untuk sebagian besar pengembang JVM (Java, Kotlin, Scala, Clojure, JRuby, Jython) jumlah informasi ini sudah cukup. Semoga sekarang Anda dapat menulis kode yang lebih baik, membuat aplikasi yang lebih efisien, menghindari berbagai masalah kebocoran memori.