Memori virtual yang digunakan oleh proses Java jauh melampaui Java Heap. Anda tahu, JVM menyertakan banyak subsistem:Pengumpul Sampah, Pemuatan Kelas, kompiler JIT, dll., dan semua subsistem ini memerlukan sejumlah RAM agar berfungsi.
JVM bukan satu-satunya konsumen RAM. Pustaka asli (termasuk Pustaka Kelas Java standar) juga dapat mengalokasikan memori asli. Dan ini bahkan tidak akan terlihat oleh Native Memory Tracking. Aplikasi Java sendiri juga dapat menggunakan memori off-heap melalui ByteBuffers langsung.
Jadi apa yang membutuhkan memori dalam proses Java?
bagian JVM (sebagian besar ditampilkan oleh Native Memory Tracking)
- Tumpukan Java
Bagian yang paling jelas. Di sinilah objek Java hidup. Heap membutuhkan hingga -Xmx
jumlah memori.
- Pengumpul Sampah
Struktur dan algoritme GC memerlukan memori tambahan untuk manajemen heap. Struktur tersebut adalah Mark Bitmap, Mark Stack (untuk melintasi grafik objek), Remembered Sets (untuk merekam referensi antar wilayah) dan lain-lain. Beberapa di antaranya dapat disetel secara langsung, mis. -XX:MarkStackSizeMax
, lainnya bergantung pada tata letak heap, mis. semakin besar wilayah G1 (-XX:G1HeapRegionSize
), semakin kecil set yang diingat.
Overhead memori GC bervariasi antara algoritma GC. -XX:+UseSerialGC
dan -XX:+UseShenandoahGC
memiliki overhead terkecil. G1 atau CMS dapat dengan mudah menggunakan sekitar 10% dari total ukuran heap.
- Cache Kode
Berisi kode yang dihasilkan secara dinamis:metode yang dikompilasi JIT, juru bahasa, dan rintisan run-time. Ukurannya dibatasi oleh -XX:ReservedCodeCacheSize
(240M secara default). Nonaktifkan -XX:-TieredCompilation
untuk mengurangi jumlah kode yang dikompilasi dan dengan demikian penggunaan Code Cache.
- Kompiler
Compiler JIT sendiri juga membutuhkan memori untuk melakukan tugasnya. Ini dapat dikurangi lagi dengan mematikan Kompilasi Berjenjang atau dengan mengurangi jumlah utas penyusun:-XX:CICompilerCount
.
- Pemuatan kelas
Metadata kelas (kode byte metode, simbol, kumpulan konstan, anotasi, dll.) disimpan di area off-heap yang disebut Metaspace. Semakin banyak kelas yang dimuat - semakin banyak metaspace yang digunakan. Penggunaan total dapat dibatasi oleh -XX:MaxMetaspaceSize
(tidak terbatas secara default) dan -XX:CompressedClassSpaceSize
(1G secara default).
- Tabel simbol
Dua hashtable utama JVM:tabel Simbol berisi nama, tanda tangan, pengidentifikasi, dll. Dan tabel String berisi referensi ke string yang diinternir. Jika Pelacakan Memori Asli menunjukkan penggunaan memori yang signifikan oleh tabel String, itu mungkin berarti aplikasi memanggil String.intern
secara berlebihan .
- Utas
Tumpukan utas juga bertanggung jawab untuk mengambil RAM. Ukuran tumpukan dikontrol oleh -Xss
. Standarnya adalah 1M per utas, tetapi untungnya tidak terlalu buruk. OS mengalokasikan halaman memori dengan malas, yaitu pada penggunaan pertama, sehingga penggunaan memori yang sebenarnya akan jauh lebih rendah (biasanya 80-200 KB per tumpukan thread). Saya menulis skrip untuk memperkirakan berapa banyak RSS milik tumpukan utas Java.
Ada bagian JVM lain yang mengalokasikan memori asli, tetapi biasanya tidak berperan besar dalam konsumsi total memori.
Buffer langsung
Aplikasi mungkin secara eksplisit meminta memori off-heap dengan memanggil ByteBuffer.allocateDirect
. Batas off-heap default sama dengan -Xmx
, tetapi dapat diganti dengan -XX:MaxDirectMemorySize
. ByteBuffer langsung disertakan dalam Other
bagian keluaran NMT (atau Internal
sebelum JDK 11).
Jumlah memori langsung yang digunakan terlihat melalui JMX, mis. di JConsole atau Java Mission Control:
Selain ByteBuffers langsung, bisa ada MappedByteBuffers
- file yang dipetakan ke memori virtual dari suatu proses. NMT tidak melacaknya, namun MappedByteBuffers juga dapat mengambil memori fisik. Dan tidak ada cara sederhana untuk membatasi berapa banyak yang dapat mereka ambil. Anda bisa melihat penggunaan sebenarnya dengan melihat peta memori proses:pmap -x <pid>
Address Kbytes RSS Dirty Mode Mapping
...
00007f2b3e557000 39592 32956 0 r--s- some-file-17405-Index.db
00007f2b40c01000 39600 33092 0 r--s- some-file-17404-Index.db
^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
Perpustakaan asli
Kode JNI dimuat oleh System.loadLibrary
dapat mengalokasikan memori off-heap sebanyak yang diinginkan tanpa kontrol dari sisi JVM. Ini juga menyangkut Perpustakaan Kelas Java standar. Secara khusus, sumber daya Java yang tidak tertutup dapat menjadi sumber kebocoran memori asli. Contoh umumnya adalah ZipInputStream
atau DirectoryStream
.
Agen JVMTI, khususnya, jdwp
agen debugging - juga dapat menyebabkan konsumsi memori yang berlebihan.
Jawaban ini menjelaskan cara membuat profil alokasi memori asli dengan async-profiler.
Masalah pengalokasi
Suatu proses biasanya meminta memori asli baik secara langsung dari OS (oleh mmap
system call) atau dengan menggunakan malloc
- pengalokasi libc standar. Pada gilirannya, malloc
meminta sebagian besar memori dari OS menggunakan mmap
, lalu mengelola potongan ini sesuai dengan algoritme alokasinya sendiri. Masalahnya adalah - algoritme ini dapat menyebabkan fragmentasi dan penggunaan memori virtual yang berlebihan.
jemalloc
, sebuah pengalokasi alternatif, seringkali terlihat lebih pintar daripada libc biasa malloc
, jadi beralihlah ke jemalloc
dapat menghasilkan footprint yang lebih kecil secara gratis.
Kesimpulan
Tidak ada cara yang pasti untuk memperkirakan penggunaan memori penuh dari proses Java, karena terlalu banyak faktor yang perlu dipertimbangkan.
Total memory = Heap + Code Cache + Metaspace + Symbol tables +
Other JVM structures + Thread stacks +
Direct buffers + Mapped files +
Native Libraries + Malloc overhead + ...
Dimungkinkan untuk menyusutkan atau membatasi area memori tertentu (seperti Cache Kode) dengan tanda JVM, tetapi banyak lainnya berada di luar kendali JVM sama sekali.
Salah satu pendekatan yang mungkin untuk menetapkan batas Docker adalah dengan memperhatikan penggunaan memori yang sebenarnya dalam keadaan proses "normal". Ada alat dan teknik untuk menyelidiki masalah penggunaan memori Java:Native Memory Tracking, pmap, jemalloc, async-profiler.
Perbarui
Ini adalah rekaman presentasi saya Memory Footprint of a Java Process.
Dalam video ini, saya membahas apa yang dapat menghabiskan memori dalam proses Java, cara memantau dan membatasi ukuran area memori tertentu, dan cara membuat profil kebocoran memori asli dalam aplikasi Java.
https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/:
Mengapa ketika saya menentukan -Xmx=1g JVM saya menggunakan lebih banyak memori daripada 1gb memori?
Menentukan -Xmx=1g memberi tahu JVM untuk mengalokasikan tumpukan 1 GB. Itu tidak memberi tahu JVM untuk membatasi seluruh penggunaan memorinya menjadi 1 GB. Ada tabel kartu, cache kode, dan segala macam struktur data off heap lainnya. Parameter yang Anda gunakan untuk menentukan penggunaan memori total adalah-XX:MaxRAM. Ketahuilah bahwa dengan -XX:MaxRam=500m heap Anda akan berukuran sekitar 250mb.
Java melihat ukuran memori host dan tidak mengetahui adanya batasan memori kontainer. Itu tidak membuat tekanan memori, jadi GC juga tidak perlu melepaskan memori bekas. Saya harap XX:MaxRAM
akan membantu Anda mengurangi jejak memori. Akhirnya, Anda dapat men-tweak konfigurasi GC (-XX:MinHeapFreeRatio
,-XX:MaxHeapFreeRatio
, ...)
Ada banyak jenis metrik memori. Docker tampaknya melaporkan ukuran memori RSS, yang mungkin berbeda dari memori "berkomitmen" yang dilaporkan oleh jcmd
(versi Docker yang lebih lama melaporkan RSS+cache sebagai penggunaan memori). Diskusi dan tautan yang bagus:Perbedaan antara Resident Set Size (RSS) dan Java total commit memory (NMT) untuk JVM yang berjalan di wadah Docker
(RSS) memori juga dapat dimakan oleh beberapa utilitas lain di dalam wadah - shell, manajer proses, ... Kami tidak tahu apa lagi yang berjalan di dalam wadah dan bagaimana Anda memulai proses dalam wadah.
TL;DR
Detail penggunaan memori disediakan oleh detail Native Memory Tracking (NMT) (terutama metadata kode dan pengumpul sampah). Selain itu, compiler dan pengoptimal Java C1/C2 menggunakan memori yang tidak dilaporkan dalam ringkasan.
Jejak memori dapat dikurangi menggunakan flag JVM (tetapi ada dampaknya).
Ukuran wadah Docker harus dilakukan melalui pengujian dengan beban aplikasi yang diharapkan.
Detail untuk setiap komponen
ruang kelas bersama dapat dinonaktifkan di dalam wadah karena kelas tidak akan dibagikan oleh proses JVM lainnya. Bendera berikut dapat digunakan. Ini akan menghapus ruang kelas bersama (17MB).
-Xshare:off
pengumpul sampah serial memiliki jejak memori minimal dengan biaya waktu jeda yang lebih lama selama pemrosesan pengumpulan sampah (lihat perbandingan Aleksey Shipilëv antara GC dalam satu gambar). Itu dapat diaktifkan dengan bendera berikut. Ini dapat menghemat hingga ruang GC yang digunakan (48MB).
-XX:+UseSerialGC
Kompiler C2 dapat dinonaktifkan dengan tanda berikut untuk mengurangi data pembuatan profil yang digunakan untuk memutuskan apakah akan mengoptimalkan atau tidak suatu metode.
-XX:+TieredCompilation -XX:TieredStopAtLevel=1
Ruang kode berkurang 20MB. Selain itu, memori di luar JVM berkurang 80MB (perbedaan antara ruang NMT dan ruang RSS). Kompiler C2 pengoptimal membutuhkan 100 MB.
Kompiler C1 dan C2 dapat dinonaktifkan dengan bendera berikut.
-Xint
Memori di luar JVM sekarang lebih rendah dari total ruang yang dikomit. Ruang kode berkurang sebesar 43MB. Hati-hati, ini berdampak besar pada kinerja aplikasi. Menonaktifkan kompiler C1 dan C2 akan mengurangi penggunaan memori sebesar 170 MB.
Menggunakan kompiler Graal VM (penggantian C2) menyebabkan jejak memori sedikit lebih kecil. Ini meningkatkan 20MB ruang memori kode dan berkurang 60MB dari luar memori JVM.
Artikel Manajemen Memori Java untuk JVM menyediakan beberapa informasi yang relevan tentang ruang memori yang berbeda. Oracle memberikan beberapa detail dalam dokumentasi Pelacakan Memori Asli. Detail lebih lanjut tentang tingkat kompilasi dalam kebijakan kompilasi lanjutan dan menonaktifkan C2 mengurangi ukuran cache kode dengan faktor 5. Beberapa detail tentang Mengapa JVM melaporkan lebih banyak memori yang dikomit daripada ukuran set residen proses Linux? ketika kedua kompiler dinonaktifkan.