Ini telah lama menjadi keluhan dengan Java, tetapi sebagian besar tidak berarti, dan biasanya didasarkan pada melihat informasi yang salah. Ungkapan yang biasa adalah seperti "Hello World on Java membutuhkan 10 megabyte! Mengapa perlu itu?" Nah, inilah cara membuat Hello World pada klaim JVM 64-bit untuk mengambil alih 4 gigabyte ... setidaknya dengan satu bentuk pengukuran.
java -Xms1024m -Xmx4096m com.example.Hello
Cara Berbeda untuk Mengukur Memori
Di Linux, perintah teratas memberi Anda beberapa nomor berbeda untuk memori. Inilah yang dikatakan tentang contoh Hello World:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2120 kgregory 20 0 4373m 15m 7152 S 0 0.2 0:00.10 java
- VIRT adalah ruang memori virtual:jumlah semua yang ada di peta memori virtual (lihat di bawah). Ini sebagian besar tidak berarti, kecuali jika tidak (lihat di bawah).
- RES adalah ukuran set residen:jumlah halaman yang saat ini residen di RAM. Di hampir semua kasus, ini adalah satu-satunya angka yang harus Anda gunakan saat mengatakan "terlalu besar". Tapi itu masih bukan angka yang bagus, terutama jika berbicara tentang Java.
- SHR adalah jumlah memori residen yang digunakan bersama dengan proses lain. Untuk proses Java, ini biasanya terbatas pada pustaka bersama dan file JAR yang dipetakan memori. Dalam contoh ini, saya hanya menjalankan satu proses Java, jadi saya menduga bahwa 7k adalah hasil pustaka yang digunakan oleh OS.
- SWAP tidak diaktifkan secara default, dan tidak ditampilkan di sini. Ini menunjukkan jumlah memori virtual yang saat ini ada di disk, apakah itu benar-benar ada atau tidak di ruang swap . OS ini sangat baik dalam mempertahankan halaman aktif dalam RAM, dan satu-satunya cara untuk menukar adalah (1) membeli lebih banyak memori, atau (2) mengurangi jumlah proses, jadi sebaiknya abaikan nomor ini.
Situasi untuk Windows Task Manager sedikit lebih rumit. Di bawah Windows XP, ada kolom "Penggunaan Memori" dan "Ukuran Memori Virtual", tetapi dokumentasi resmi tidak menyebutkan artinya. Windows Vista dan Windows 7 menambahkan lebih banyak kolom, dan mereka benar-benar didokumentasikan. Dari jumlah tersebut, pengukuran "Set Kerja" adalah yang paling berguna; kira-kira sama dengan jumlah RES dan SHR di Linux.
Memahami Peta Memori Virtual
Memori virtual yang dikonsumsi oleh suatu proses adalah total dari semua yang ada di peta memori proses. Ini termasuk data (misalnya, tumpukan Java), tetapi juga semua pustaka bersama dan file yang dipetakan memori yang digunakan oleh program. Di Linux, Anda dapat menggunakan perintah pmap untuk melihat semua hal yang dipetakan ke dalam ruang proses (mulai dari sini saya hanya akan merujuk ke Linux, karena itulah yang saya gunakan; saya yakin ada alat yang setara untuk jendela). Berikut kutipan dari peta memori program "Hello World"; seluruh peta memori memiliki panjang lebih dari 100 baris, dan bukan hal yang aneh jika memiliki daftar seribu baris.
0000000040000000 36K r-x-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040108000 8K rwx-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040eba000 676K rwx-- [ anon ] 00000006fae00000 21248K rwx-- [ anon ] 00000006fc2c0000 62720K rwx-- [ anon ] 0000000700000000 699072K rwx-- [ anon ] 000000072aab0000 2097152K rwx-- [ anon ] 00000007aaab0000 349504K rwx-- [ anon ] 00000007c0000000 1048576K rwx-- [ anon ] ... 00007fa1ed00d000 1652K r-xs- /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar ... 00007fa1ed1d3000 1024K rwx-- [ anon ] 00007fa1ed2d3000 4K ----- [ anon ] 00007fa1ed2d4000 1024K rwx-- [ anon ] 00007fa1ed3d4000 4K ----- [ anon ] ... 00007fa1f20d3000 164K r-x-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so 00007fa1f20fc000 1020K ----- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so 00007fa1f21fb000 28K rwx-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so ... 00007fa1f34aa000 1576K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3833000 16K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3837000 4K rwx-- /lib/x86_64-linux-gnu/libc-2.13.so ...
Penjelasan singkat tentang formatnya:setiap baris dimulai dengan alamat memori virtual dari segmen tersebut. Ini diikuti oleh ukuran segmen, izin, dan sumber segmen. Item terakhir ini adalah file atau "anon", yang menunjukkan blok memori yang dialokasikan melalui mmap.
Mulai dari atas, kami memiliki
- Pemuat JVM (yaitu, program yang dijalankan saat Anda mengetik
java
). Ini sangat kecil; yang dilakukannya hanyalah memuat di pustaka bersama tempat kode JVM asli disimpan. - Sekumpulan blok anon yang menyimpan heap Java dan data internal. Ini adalah Sun JVM, jadi heap dipecah menjadi beberapa generasi, yang masing-masing merupakan blok memorinya sendiri. Perhatikan bahwa JVM mengalokasikan ruang memori virtual berdasarkan
-Xmx
nilai; ini memungkinkannya memiliki tumpukan yang berdekatan.-Xms
value digunakan secara internal untuk mengatakan berapa banyak heap yang "sedang digunakan" saat program dimulai, dan untuk memicu pengumpulan sampah saat mendekati batas tersebut. - Berkas JAR yang dipetakan memori, dalam hal ini berkas yang menyimpan "kelas JDK". Saat Anda memetakan memori JAR, Anda dapat mengakses file di dalamnya dengan sangat efisien (dibandingkan membacanya dari awal setiap saat). Sun JVM akan memetakan memori semua JAR di classpath; jika kode aplikasi Anda perlu mengakses JAR, Anda juga dapat memetakannya dengan memori.
- Data per utas untuk dua utas. Blok 1M adalah tumpukan utas. Saya tidak memiliki penjelasan yang baik untuk blok 4k, tetapi @ericsoe mengidentifikasinya sebagai "blok penjaga":tidak memiliki izin baca/tulis, sehingga akan menyebabkan kesalahan segmen jika diakses, dan JVM menangkapnya dan menerjemahkannya ke
StackOverFlowError
. Untuk aplikasi nyata, Anda akan melihat lusinan bahkan ratusan entri ini diulang melalui peta memori. - Salah satu pustaka bersama yang menyimpan kode JVM sebenarnya. Ada beberapa di antaranya.
- Pustaka bersama untuk pustaka standar C. Ini hanyalah salah satu dari banyak hal yang dimuat JVM yang tidak sepenuhnya merupakan bagian dari Java.
Pustaka bersama sangat menarik:setiap pustaka bersama memiliki setidaknya dua segmen:segmen baca-saja yang berisi kode pustaka, dan segmen baca-tulis yang berisi data per-proses global untuk pustaka (saya tidak tahu apa segmen tanpa izin adalah; Saya hanya melihatnya di x64 Linux). Porsi baca-saja dari pustaka dapat dibagikan di antara semua proses yang menggunakan pustaka; misalnya, libc
memiliki 1,5 juta ruang memori virtual yang dapat digunakan bersama.
Kapan Ukuran Memori Virtual Penting?
Peta memori virtual berisi banyak hal. Beberapa di antaranya bersifat read-only, beberapa dibagikan, dan beberapa dialokasikan tetapi tidak pernah disentuh (misalnya, hampir semua tumpukan 4Gb dalam contoh ini). Namun sistem operasi cukup pintar untuk hanya memuat apa yang diperlukan, sehingga ukuran memori virtual sebagian besar tidak relevan.
Di mana ukuran memori virtual penting adalah jika Anda menjalankan sistem operasi 32-bit, di mana Anda hanya dapat mengalokasikan 2Gb (atau, dalam beberapa kasus, 3Gb) ruang alamat proses. Dalam hal ini, Anda berurusan dengan sumber daya yang langka, dan mungkin harus melakukan pengorbanan, seperti mengurangi ukuran heap untuk memetakan memori file besar atau membuat banyak utas.
Namun, mengingat mesin 64-bit ada di mana-mana, menurut saya tidak lama lagi Ukuran Memori Virtual menjadi statistik yang sama sekali tidak relevan.
Kapan Ukuran Resident Set Penting?
Ukuran Resident Set adalah bagian dari ruang memori virtual yang sebenarnya ada di RAM. Jika RSS Anda menjadi bagian yang signifikan dari total memori fisik Anda, mungkin sudah waktunya untuk mulai khawatir. Jika RSS Anda semakin memakan semua memori fisik Anda, dan sistem Anda mulai bertukar, sudah lewat waktu untuk mulai khawatir.
Tapi RSS juga menyesatkan, terutama pada mesin yang dimuat ringan. Sistem operasi tidak mengeluarkan banyak upaya untuk mendapatkan kembali halaman yang digunakan oleh suatu proses. Ada sedikit manfaat yang bisa diperoleh dengan melakukannya, dan potensi kesalahan halaman yang mahal jika proses menyentuh halaman di masa mendatang. Akibatnya, statistik RSS mungkin berisi banyak halaman yang tidak digunakan secara aktif.
Intinya
Kecuali jika Anda bertukar, jangan terlalu khawatir tentang apa yang dikatakan oleh berbagai statistik memori kepada Anda. Dengan peringatan bahwa RSS yang terus berkembang dapat mengindikasikan semacam kebocoran memori.
Dengan program Java, jauh lebih penting untuk memperhatikan apa yang terjadi di heap. Jumlah total ruang yang dikonsumsi penting, dan ada beberapa langkah yang dapat Anda ambil untuk menguranginya. Yang lebih penting adalah jumlah waktu yang Anda habiskan dalam pengumpulan sampah, dan bagian tumpukan mana yang dikumpulkan.
Mengakses disk (yaitu database) mahal, dan memori murah. Jika Anda dapat menukar satu sama lain, lakukanlah.
Jumlah memori yang dialokasikan untuk proses Java hampir setara dengan yang saya harapkan. Saya mengalami masalah serupa dalam menjalankan Java pada sistem terbatas tertanam/memori. Menjalankan apa saja aplikasi dengan batas VM sewenang-wenang atau pada sistem yang tidak memiliki jumlah swap yang memadai cenderung rusak. Tampaknya sifat dari banyak aplikasi modern tidak dirancang untuk digunakan pada sistem terbatas sumber daya.
Anda memiliki beberapa opsi lagi yang dapat Anda coba dan batasi jejak memori JVM Anda. Ini mungkin mengurangi jejak memori virtual:
-XX:ReservedCodeCacheSize=32m Ukuran cache kode yang dicadangkan (dalam byte) - ukuran cache kode maksimum. [Solaris 64-bit, amd64, dan -server x86:48m; in1.5.0_06 dan sebelumnya, Solaris 64-bit dan and64:1024m.]
-XX:MaxPermSize=64m Ukuran Generasi Permanen. [5.0 dan yang lebih baru:VM 64 bit diskalakan 30% lebih besar; 1,4amd64:96m; 1.3.1 -klien:32m.]
Selain itu, Anda juga harus menyetel -Xmx (max heap size) ke nilai yang sedekat mungkin dengan penggunaan memori puncak sebenarnya aplikasi Anda. Saya yakin perilaku default JVM masih menggandakan ukuran tumpukan setiap kali mengembang hingga maksimal. Jika Anda memulai dengan heap 32 juta dan aplikasi Anda mencapai puncaknya menjadi 65 juta, maka heap akan tumbuh menjadi 32 juta -> 64 juta -> 128 juta.
Anda juga dapat mencoba ini agar VM tidak terlalu agresif dalam mengembangkan heap:
-XX:MinHeapFreeRatio=40 Persentase minimum bebas heap setelah GC untuk menghindari ekspansi.
Juga, dari apa yang saya ingat dari percobaan dengan ini beberapa tahun yang lalu, jumlah perpustakaan asli yang dimuat berdampak besar pada jejak minimum. Memuat java.net.Socket menambahkan lebih dari 15 juta jika saya ingat dengan benar (dan mungkin tidak).
Ada masalah umum dengan Java dan glibc>=2.10 (termasuk Ubuntu>=10.04, RHEL>=6).
Obatnya adalah mengatur env ini. variabel:
export MALLOC_ARENA_MAX=4
Jika Anda menjalankan Tomcat, Anda dapat menambahkan ini ke TOMCAT_HOME/bin/setenv.sh
berkas.
Untuk Docker, tambahkan ini ke Dockerfile
ENV MALLOC_ARENA_MAX=4
Ada artikel IBM tentang pengaturan MALLOC_ARENA_MAXhttps://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en
Entri blog ini menyatakan
memori residen diketahui merayap dengan cara yang mirip dengan kebocoran memori atau fragmentasi memori.
Ada juga bug JDK terbuka JDK-8193521 "glibc membuang-buang memori dengan konfigurasi default"
cari MALLOC_ARENA_MAX di Google atau SO untuk referensi lebih lanjut.
Anda mungkin ingin menyetel juga opsi malloc lainnya untuk mengoptimalkan fragmentasi rendah dari memori yang dialokasikan:
# tune glibc memory allocation, optimize for low fragmentation
# limit the number of arenas
export MALLOC_ARENA_MAX=2
# disable dynamic mmap threshold, see M_MMAP_THRESHOLD in "man mallopt"
export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536