Untuk aplikasi saya, memori yang digunakan oleh proses Java jauh lebih banyak daripada ukuran heap.
Sistem tempat container dijalankan mulai mengalami masalah memori karena container menggunakan lebih banyak memori daripada ukuran heap.
Ukuran heap disetel ke 128 MB ( -Xmx128m -Xms128m
) sedangkan penampung membutuhkan hingga 1 GB memori. Dalam kondisi normal, dibutuhkan 500MB. Jika kontainer buruh pelabuhan memiliki batas di bawah ini (misalnya mem_limit=mem_limit=400MB
) proses dimatikan oleh pembunuh memori keluar dari OS.
Bisakah Anda menjelaskan mengapa proses Java menggunakan lebih banyak memori daripada heap? Bagaimana mengukur dengan benar batas memori Docker? Apakah ada cara untuk mengurangi jejak memori off-heap dari proses Java?
Saya mengumpulkan beberapa detail tentang masalah menggunakan perintah dari pelacakan memori asli di JVM .
Dari sistem host, saya mendapatkan memori yang digunakan oleh container.
$ docker stats --no-stream 9afcb62a26c8
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
9afcb62a26c8 xx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.0acbb46bb6fe3ae1b1c99aff3a6073bb7b7ecf85 0.93% 461MiB / 9.744GiB 4.62% 286MB / 7.92MB 157MB / 2.66GB 57
Dari dalam wadah, saya mendapatkan memori yang digunakan oleh proses tersebut.
$ ps -p 71 -o pcpu,rss,size,vsize
%CPU RSS SIZE VSZ
11.2 486040 580860 3814600
$ jcmd 71 VM.native_memory
71:
Native Memory Tracking:
Total: reserved=1631932KB, committed=367400KB
- Java Heap (reserved=131072KB, committed=131072KB)
(mmap: reserved=131072KB, committed=131072KB)
- Class (reserved=1120142KB, committed=79830KB)
(classes #15267)
( instance classes #14230, array classes #1037)
(malloc=1934KB #32977)
(mmap: reserved=1118208KB, committed=77896KB)
( Metadata: )
( reserved=69632KB, committed=68272KB)
( used=66725KB)
( free=1547KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=1048576KB, committed=9624KB)
( used=8939KB)
( free=685KB)
( waste=0KB =0.00%)
- Thread (reserved=24786KB, committed=5294KB)
(thread #56)
(stack: reserved=24500KB, committed=5008KB)
(malloc=198KB #293)
(arena=88KB #110)
- Code (reserved=250635KB, committed=45907KB)
(malloc=2947KB #13459)
(mmap: reserved=247688KB, committed=42960KB)
- GC (reserved=48091KB, committed=48091KB)
(malloc=10439KB #18634)
(mmap: reserved=37652KB, committed=37652KB)
- Compiler (reserved=358KB, committed=358KB)
(malloc=249KB #1450)
(arena=109KB #5)
- Internal (reserved=1165KB, committed=1165KB)
(malloc=1125KB #3363)
(mmap: reserved=40KB, committed=40KB)
- Other (reserved=16696KB, committed=16696KB)
(malloc=16696KB #35)
- Symbol (reserved=15277KB, committed=15277KB)
(malloc=13543KB #180850)
(arena=1734KB #1)
- Native Memory Tracking (reserved=4436KB, committed=4436KB)
(malloc=378KB #5359)
(tracking overhead=4058KB)
- Shared class space (reserved=17144KB, committed=17144KB)
(mmap: reserved=17144KB, committed=17144KB)
- Arena Chunk (reserved=1850KB, committed=1850KB)
(malloc=1850KB)
- Logging (reserved=4KB, committed=4KB)
(malloc=4KB #179)
- Arguments (reserved=19KB, committed=19KB)
(malloc=19KB #512)
- Module (reserved=258KB, committed=258KB)
(malloc=258KB #2356)
$ cat /proc/71/smaps | grep Rss | cut -d: -f2 | tr -d " " | cut -f1 -dk | sort -n | awk '{ sum += $1 } END { print sum }'
491080
Aplikasi ini adalah server web menggunakan Jetty / Jersey / CDI yang dibundel dengan ukuran file sebesar 36 MB.
Versi OS dan Java berikut digunakan (di dalam penampung). Gambar Docker didasarkan pada openjdk:11-jre-slim
.
$ java -version
openjdk version "11" 2018-09-25
OpenJDK Runtime Environment (build 11+28-Debian-1)
OpenJDK 64-Bit Server VM (build 11+28-Debian-1, mixed mode, sharing)
$ uname -a
Linux service1 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 GNU/Linux
https://gist.github.com/prasanthj/48e7063cac88eb396bc9961fb3149b58
cgroups
menambahkan disk-cache ke memori yang digunakan - bahkan jika itu ditangani oleh kernel dan itu tidak terlihat oleh program pengguna. (Ingat, perintahkanps
dandocker stats
jangan hitung disk-cache.)Jawaban:
Memori virtual yang digunakan oleh proses Java jauh melampaui Java Heap. Anda tahu, JVM menyertakan banyak subsitem: Pengumpul Sampah, Pemuatan Kelas, kompiler JIT, dll., Dan semua subsistem ini memerlukan sejumlah RAM untuk berfungsi.
JVM bukan satu-satunya konsumen RAM. Perpustakaan asli (termasuk Perpustakaan Kelas Java standar) juga dapat mengalokasikan memori asli. Dan ini bahkan tidak akan terlihat oleh Pelacakan Memori Asli. Aplikasi Java sendiri juga dapat menggunakan memori off-heap melalui ByteBuffers langsung.
Jadi apa yang membutuhkan memori dalam proses Java?
Bagian JVM (sebagian besar ditunjukkan oleh Native Memory Tracking)
Java Heap
Bagian yang paling jelas. Di sinilah objek Java hidup. Heap membutuhkan
-Xmx
sejumlah memori.Pengumpul sampah
Struktur dan algoritme GC memerlukan memori tambahan untuk manajemen heap. Struktur tersebut adalah Mark Bitmap, Mark Stack (untuk traversing object graph), Remembered Sets (untuk merekam referensi antarwilayah) dan lain-lain. Beberapa di antaranya dapat disetel secara langsung, misalnya
-XX:MarkStackSizeMax
, yang lain bergantung pada tata letak heap, misalnya semakin besar region G1 (-XX:G1HeapRegionSize
), semakin kecil set yang diingat.Overhead memori GC bervariasi antara algoritme GC.
-XX:+UseSerialGC
dan-XX:+UseShenandoahGC
memiliki biaya overhead terkecil. G1 atau CMS dapat dengan mudah menggunakan sekitar 10% dari total ukuran heap.Cache Kode
Berisi kode yang dibuat secara dinamis: metode yang dikompilasi JIT, interpreter, dan stub run-time. Ukurannya dibatasi oleh
-XX:ReservedCodeCacheSize
(240M secara default). Matikan-XX:-TieredCompilation
untuk mengurangi jumlah kode yang dikompilasi dan dengan demikian penggunaan Cache Kode.Penyusun
Kompiler JIT sendiri juga membutuhkan memori untuk melakukan tugasnya. Ini dapat dikurangi lagi dengan mematikan berjenjang Kompilasi atau dengan mengurangi jumlah thread compiler:
-XX:CICompilerCount
.Pembebanan kelas
Metadata kelas (bytecode metode, simbol, kumpulan konstan, anotasi, dll.) Disimpan di area off-heap yang disebut Metaspace. Semakin banyak kelas yang dimuat - semakin banyak metaspace yang digunakan. Total penggunaan dapat dibatasi oleh
-XX:MaxMetaspaceSize
(tidak terbatas secara default) dan-XX:CompressedClassSpaceSize
(1G secara default).Tabel simbol
Dua hashtable utama JVM: tabel Symbol berisi nama, tanda tangan, pengenal, dll. Dan tabel String berisi referensi ke string internal. Jika Native Memory Tracking menunjukkan penggunaan memori yang signifikan oleh tabel String, itu mungkin berarti aplikasi memanggil secara berlebihan
String.intern
.Benang
Tumpukan benang juga bertanggung jawab untuk mengambil RAM. Ukuran tumpukan dikontrol oleh
-Xss
. Standarnya adalah 1 juta per utas, tetapi untungnya semuanya tidak terlalu buruk. OS mengalokasikan halaman memori dengan malas, yaitu pada penggunaan pertama, sehingga penggunaan memori sebenarnya akan jauh lebih rendah (biasanya 80-200 KB per tumpukan thread). Saya menulis skrip untuk memperkirakan berapa banyak RSS milik tumpukan thread Java.Ada bagian JVM lain yang mengalokasikan memori asli, tetapi biasanya tidak berperan besar dalam konsumsi memori total.
Buffer langsung
Aplikasi mungkin secara eksplisit meminta memori off-heap dengan memanggil
ByteBuffer.allocateDirect
. Batas off-heap default adalah sama dengan-Xmx
, tetapi dapat diganti dengan-XX:MaxDirectMemorySize
. Direct ByteBuffers termasuk dalamOther
bagian output NMT (atauInternal
sebelum JDK 11).Jumlah memori langsung yang digunakan terlihat melalui JMX, misalnya di JConsole atau Java Mission Control:
Selain ByteBuffers langsung, mungkin ada
MappedByteBuffers
- file yang dipetakan ke memori virtual dari suatu proses. NMT tidak melacaknya, namun MappedByteBuffers juga dapat menggunakan 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>
Perpustakaan asli
Kode JNI yang 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, resource Java yang tidak ditutup dapat menjadi sumber kebocoran memori native. Contoh umumnya adalahZipInputStream
atauDirectoryStream
.Agen JVMTI, khususnya,
jdwp
agen debugging - juga dapat menyebabkan konsumsi memori yang berlebihan.Jawaban ini menjelaskan cara membuat profil alokasi memori native dengan async-profiler .
Masalah alokator
Suatu proses biasanya meminta memori asli baik secara langsung dari OS (dengan
mmap
panggilan sistem) atau dengan menggunakanmalloc
- pengalokasi libc standar. Pada gilirannya,malloc
meminta potongan besar memori dari OS yang digunakanmmap
, dan kemudian mengelola potongan ini sesuai dengan algoritme alokasinya sendiri. Masalahnya adalah - algoritma ini dapat menyebabkan fragmentasi dan penggunaan memori virtual yang berlebihan .jemalloc
, pengalokasi alternatif, sering kali tampak lebih pintar daripada libc biasamalloc
, jadi beralih kejemalloc
dapat menghasilkan footprint yang lebih kecil secara gratis.Kesimpulan
Tidak ada cara yang dijamin untuk memperkirakan penggunaan memori penuh dari proses Java, karena ada terlalu banyak faktor yang perlu dipertimbangkan.
Dimungkinkan untuk mengecilkan atau membatasi area memori tertentu (seperti Cache Kode) dengan flag JVM, tetapi banyak lainnya berada di luar kendali JVM sama sekali.
Salah satu pendekatan yang mungkin untuk menetapkan batas Docker adalah dengan melihat penggunaan memori aktual dalam proses "normal". Ada alat dan teknik untuk menyelidiki masalah dengan konsumsi memori Java: Pelacakan Memori Native , pmap , jemalloc , async-profiler .
Memperbarui
Berikut adalah rekaman presentasi saya Memory Footprint of a Java Process .
Dalam video ini, saya membahas apa yang mungkin mengonsumsi memori dalam proses Java, cara memantau dan menahan ukuran area memori tertentu, dan cara membuat profil kebocoran memori asli dalam aplikasi Java.
sumber
https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/ :
Java melihat ukuran memori host dan tidak mengetahui adanya batasan memori container. 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 dapat berbeda dari memori "berkomitmen" yang dilaporkan oleh
jcmd
(versi Docker 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 container DockerMemori (RSS) juga dapat dimakan oleh beberapa utilitas lain di container - shell, process manager, ... Kami tidak tahu apa lagi yang berjalan di container dan bagaimana Anda memulai proses di container.
sumber
-XX:MaxRam
. Saya pikir ini masih menggunakan lebih dari yang ditentukan maksimum tetapi lebih baik, terima kasih!-Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC
, hasilDocker 428.5MiB / 600MiB
danjcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KB
. JVM membutuhkan sekitar 300MB sementara kontainer membutuhkan 430MB. Di mana 130MB antara pelaporan JVM dan pelaporan OS?ps -p 71 -o pcpu,rss,size,vsize
dengan proses Java yang memiliki pid 71. Sebenarnya-XX:MaxRam
tidak membantu tetapi tautan yang Anda berikan membantu dengan serial GC.TL; DR
Penggunaan detail 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 dengan menggunakan tanda JVM (tetapi ada dampaknya).
Ukuran kontainer Docker harus dilakukan melalui pengujian dengan pemuatan aplikasi yang diharapkan.
Detail untuk setiap komponen
Ruang kelas bersama dapat dinonaktifkan di dalam wadah karena kelas tidak akan dibagikan oleh proses JVM lain. Bendera berikut dapat digunakan. Ini akan menghapus ruang kelas bersama (17MB).
Serial pengumpul sampah 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).
The C2 compiler dapat dinonaktifkan dengan bendera berikut untuk mengurangi profil data yang digunakan untuk memutuskan apakah akan mengoptimalkan atau tidak metode.
Ruang kode dikurangi 20MB. Selain itu, memori di luar JVM berkurang hingga 80MB (perbedaan antara ruang NMT dan ruang RSS). Kompiler C2 yang mengoptimalkan membutuhkan 100MB.
The C1 dan C2 compiler dapat dinonaktifkan dengan bendera berikut.
Memori di luar JVM sekarang lebih rendah dari total ruang yang dikomitmenkan. Ruang kode dikurangi 43MB. Hati-hati, ini berdampak besar pada kinerja aplikasi. Menonaktifkan compiler C1 dan C2 akan mengurangi penggunaan memori sebesar 170 MB.
Menggunakan kompiler Graal VM (penggantian C2) mengarah ke footprint memori yang sedikit lebih kecil. Ini meningkatkan 20MB ruang memori kode dan mengurangi 60MB dari luar memori JVM.
Artikel Manajemen Memori Java untuk JVM memberikan beberapa informasi yang relevan tentang ruang memori yang berbeda. Oracle memberikan beberapa detail dalam dokumentasi Native Memory Tracking . Detail lebih lanjut tentang tingkat kompilasi dalam kebijakan kompilasi lanjutan dan dalam menonaktifkan C2 mengurangi ukuran cache kode dengan faktor 5 . Beberapa detail tentang Mengapa JVM melaporkan lebih banyak memori yang berkomitmen daripada ukuran yang ditetapkan tetap proses Linux? ketika kedua kompiler dinonaktifkan.
sumber
Java membutuhkan banyak memori. JVM sendiri membutuhkan banyak memori untuk dijalankan. Heap adalah memori yang tersedia di dalam mesin virtual, tersedia untuk aplikasi Anda. Karena JVM adalah paket besar yang dikemas dengan semua barang yang memungkinkan, dibutuhkan banyak memori hanya untuk memuat.
Dimulai dengan java 9 Anda memiliki sesuatu yang disebut proyek Jigsaw , yang mungkin mengurangi memori yang digunakan saat Anda memulai aplikasi java (bersama dengan waktu mulai). Proyek jigsaw dan sistem modul baru belum tentu dibuat untuk mengurangi memori yang diperlukan, tetapi jika penting Anda bisa mencobanya.
Anda dapat melihat contoh ini: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/ . Dengan menggunakan sistem modul menghasilkan aplikasi CLI sebesar 21MB (dengan JRE embed). JRE membutuhkan lebih dari 200mb. Itu seharusnya diterjemahkan ke memori yang dialokasikan lebih sedikit saat aplikasi aktif (banyak kelas JRE yang tidak digunakan tidak akan dimuat lagi).
Berikut adalah tutorial bagus lainnya: https://www.baeldung.com/project-jigsaw-java-modularity
Jika Anda tidak ingin menghabiskan waktu dengan ini, Anda dapat mengalokasikan lebih banyak memori. Terkadang itu yang terbaik.
sumber
jlink
cukup terbatas karena memerlukan aplikasi untuk menjadi modularis. Modul otomatis tidak didukung sehingga tidak ada cara mudah untuk pergi ke sana.Bagaimana mengukur dengan benar batas memori Docker? Periksa aplikasi dengan memantaunya selama beberapa waktu. Untuk membatasi memori penampung coba gunakan opsi -m, --memory bytes untuk perintah jalankan buruh pelabuhan - atau sesuatu yang setara jika Anda menjalankannya sebaliknya seperti
tidak bisa menjawab pertanyaan lain.
sumber