Saya tidak sengaja menemukan definisi "Babi dalam Python (Catatan: Ini seperti ular yang rakus dan tidak mampu menelan gajah)" dalam bahasa Mandarin ketika saya melihat glosarium manajemen memori, jadi saya membuat artikel ini. Di permukaan, istilah ini mengacu pada GC yang terus-menerus mempromosikan benda-benda besar dari satu generasi ke generasi lainnya. Melakukan hal ini seperti ular piton yang menelan mangsanya utuh-utuh, sehingga tidak bisa bergerak saat mencernanya.
Selama 24 jam berikutnya, pikiran saya dipenuhi dengan gambaran ular piton yang menyesakkan ini yang tidak dapat saya hilangkan. Seperti yang dikatakan para psikiater, cara terbaik untuk menghilangkan rasa takut adalah dengan membicarakannya. Oleh karena itu artikel ini. Namun cerita selanjutnya yang ingin kita bicarakan bukanlah python, melainkan tuning GC. Aku bersumpah demi Tuhan.
Semua orang tahu bahwa jeda GC dapat dengan mudah menyebabkan hambatan kinerja. JVM modern hadir dengan pengumpul sampah tingkat lanjut saat dirilis, namun berdasarkan pengalaman saya, sangat sulit menemukan konfigurasi optimal untuk aplikasi tertentu. Penyetelan manual mungkin masih memiliki secercah harapan, tetapi Anda harus memahami mekanisme sebenarnya dari algoritma GC. Dalam hal ini, artikel ini akan bermanfaat bagi Anda. Di bawah ini saya akan menggunakan contoh untuk menjelaskan bagaimana perubahan kecil pada konfigurasi JVM mempengaruhi throughput aplikasi Anda.
Contoh
Aplikasi yang kami gunakan untuk mendemonstrasikan dampak GC pada throughput adalah program sederhana. Ini berisi dua utas:
PigEater Ini akan meniru proses ular piton raksasa memakan babi besar yang gemuk. Kode melakukan ini dengan menambahkan 32MB byte ke java.util.List dan tidur selama 100 mdtk setelah setiap menelan.
PigDgester Ini mensimulasikan proses pencernaan asinkron. Kode yang mengimplementasikan pencernaan hanya mengatur daftar babi menjadi kosong. Karena ini adalah proses yang melelahkan, thread ini akan tidur selama 2000 ms setiap kali setelah menghapus referensi.
Kedua thread akan berjalan dalam satu putaran, memakan dan mencerna sampai ularnya kenyang. Ini akan membutuhkan makan sekitar 5.000 babi.
Copy kode kodenya sebagai berikut:
paket eu.plumbr.demo;
kelas publik PigInThePython {
static volatil Daftar babi = new ArrayList();
static volatil int pigsEaten = 0;
int akhir statis ENOUGH_PIGS = 5000;
public static void main(String[] args) melempar InterruptedException {
Pemakan Babi baru().mulai();
PigDigester baru().start();
}
kelas statis PigEater memperluas Thread {
@Mengesampingkan
menjalankan kekosongan publik() {
sementara (benar) {
pigs.add(byte baru[32 * 1024 * 1024]); //32MB per babi
if (pigsEaten > ENOUGH_PIGS) kembali;
ambilANap(100);
}
}
}
kelas statis PigDigester memperluas Thread {
@Mengesampingkan
menjalankan kekosongan publik() {
mulai panjang = System.currentTimeMillis();
sementara (benar) {
ambilANap(2000);
babiDimakan+=babi.ukuran();
babi = ArrayList baru();
if (babiDimakan > ENOUGH_PIGS) {
System.out.format("Mencerna %d babi dalam %d ms.%n",pigsEaten, System.currentTimeMillis()-start);
kembali;
}
}
}
}
kekosongan statis takeANap(int ms) {
mencoba {
Thread.tidur(ms);
} tangkapan (Pengecualian e) {
e.printStackTrace();
}
}
}
Sekarang kami mendefinisikan throughput sistem ini sebagai "jumlah babi yang dapat dicerna per detik". Mengingat seekor babi dimasukkan ke dalam python ini setiap 100 ms, kita dapat melihat bahwa throughput maksimum teoretis dari sistem ini dapat mencapai 10 babi/detik.
Contoh konfigurasi GC
Mari kita lihat kinerja menggunakan dua sistem konfigurasi yang berbeda. Terlepas dari konfigurasinya, aplikasi ini berjalan pada Mac dual-core (OS X10.9.3) dengan RAM 8 GB.
Konfigurasi pertama:
tumpukan 1,4G (-Xms4g -Xmx4g)
2. Gunakan CMS untuk membersihkan generasi lama (-XX:+UseConcMarkSweepGC) dan gunakan kolektor paralel untuk membersihkan generasi baru (-XX:+UseParNewGC)
3. Alokasikan 12,5% heap (-Xmn512m) ke generasi baru, dan batasi ukuran area Eden dan area Survivor agar sama.
Konfigurasi kedua sedikit berbeda:
tumpukan 1,2G (-Xms2g -Xms2g)
2. Baik generasi baru maupun generasi lama menggunakan Parellel GC (-XX:+UseParallelGC)
3. Alokasikan 75% heap ke generasi baru (-Xmn 1536m)
4. Sekarang saatnya bertaruh, konfigurasi mana yang berkinerja lebih baik (ingat berapa banyak babi yang bisa dimakan per detik)? Mereka yang memasang chipnya pada konfigurasi pertama, Anda akan kecewa. Hasilnya justru sebaliknya:
1. Konfigurasi pertama (tumpukan besar, generasi lama besar, CMS GC) dapat memakan 8,2 babi per detik
2. Konfigurasi kedua (tumpukan kecil, generasi baru besar, Parellel GC) dapat memakan 9,2 babi per detik
Sekarang mari kita lihat hasil ini secara objektif. Sumber daya yang dialokasikan 2 kali lebih sedikit tetapi throughputnya meningkat sebesar 12%. Hal ini bertentangan dengan akal sehat, sehingga perlu dianalisis lebih lanjut apa yang terjadi.
Analisis hasil GC
Alasannya sebenarnya tidak rumit. Anda bisa mengetahui jawabannya dengan melihat lebih dekat apa yang dilakukan GC saat menjalankan pengujian. Di sinilah Anda memilih alat yang ingin Anda gunakan. Dengan bantuan jstat, saya menemukan rahasia di baliknya. Perintahnya mungkin seperti ini:
Copy kode kodenya sebagai berikut:
jstat -gc -t -h20 PID 1dtk
Menganalisis data, saya perhatikan bahwa konfigurasi 1 melewati 1129 siklus GC (YGCT_FGCT), dengan total waktu 63,723 detik:
Copy kode kodenya sebagai berikut:
Stempel waktu S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
594,0 174720,0 174720,0 163844,1 0,0 174848,0 131074,1 3670016,0 2621693,5 21248,0 2580,9 1006 63,182 116 0,236 63.419
595,0 174720,0 174720,0 163842,1 0,0 174848,0 65538,0 3670016,0 3047677,9 21248,0 2580,9 1008 63,310 117 0,236 63.546
596,1 174720,0 174720,0 98308,0 163842,1 174848,0 163844,2 3670016,0 491772,9 21248,0 2580,9 1010 63,354 118 0,240 63.595
597,0 174720,0 174720,0 0,0 163840,1 174848,0 131074,1 3670016,0 688380,1 21248,0 2580,9 1011 63,482 118 0,240 63.723
Konfigurasi kedua dijeda sebanyak 168 kali (YGCT+FGCT) dan hanya membutuhkan waktu 11,409 detik.
Copy kode kodenya sebagai berikut:
Stempel waktu S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
539,3 164352,0 164352,0 0,0 0,0 1211904,0 98306,0 524288,0 164352,2 21504,0 2579,2 27 2,969 141 8,441 11,409
540,3 164352,0 164352,0 0,0 0,0 1211904,0 425986,2 524288,0 164352,2 21504,0 2579,2 27 2,969 141 8,441 11,409
541,4 164352,0 164352,0 0,0 0,0 1211904,0 720900,4 524288,0 164352,2 21504,0 2579,2 27 2,969 141 8,441 11,409
542,3 164352,0 164352,0 0,0 0,0 1211904,0 1015812,6 524288,0 164352,2 21504,0 2579,2 27 2,969 141 8,441 11,409
Mengingat beban kerja pada kedua kasus tersebut sama, maka - dalam eksperimen makan babi ini, ketika GC tidak menemukan objek berumur panjang, GC dapat membersihkan objek sampah dengan lebih cepat. Dengan konfigurasi pertama, frekuensi operasi GC akan menjadi sekitar 6 hingga 7 kali, dan total waktu jeda akan menjadi 5 hingga 6 kali.
Menceritakan kisah ini memiliki dua tujuan. Pertama dan terpenting, saya ingin menghilangkan ular piton yang mengejang ini dari pikiran saya. Keuntungan lain yang lebih nyata adalah bahwa penyetelan GC merupakan pengalaman yang sangat terampil, dan mengharuskan Anda memiliki pemahaman menyeluruh tentang konsep yang mendasarinya. Meskipun yang digunakan dalam artikel ini hanyalah aplikasi yang sangat umum, hasil seleksi yang berbeda juga akan berdampak besar pada perencanaan throughput dan kapasitas Anda. Dalam penerapan di kehidupan nyata, perbedaannya di sini akan lebih besar. Jadi terserah Anda, Anda bisa menguasai konsep-konsep ini, atau Anda bisa fokus pada pekerjaan sehari-hari dan biarkan Plumbr menemukan konfigurasi GC yang paling sesuai dengan kebutuhan Anda.