Java menggunakan lebih banyak memori daripada ukuran heap (atau ukuran dengan benar batas memori Docker)

118

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

Nicolas Henneaux
sumber
6
Heap adalah tempat objek dialokasikan, namun JVM memiliki banyak region memori lainnya termasuk library bersama, buffer memori langsung, tumpukan thread, komponen GUI, metaspace. Anda perlu melihat seberapa besar JVM dan membuat batasnya cukup tinggi sehingga Anda lebih suka proses mati daripada menggunakannya lagi.
Peter Lawrey
2
Sepertinya GC menggunakan banyak memori. Sebagai gantinya, Anda dapat mencoba menggunakan pengumpul CMS. Sepertinya ~ 125 MB digunakan untuk metaspace + kode, namun tanpa menyusutkan basis kode Anda, Anda tidak mungkin dapat membuatnya lebih kecil. Ruang yang dikomitmenkan mendekati batas Anda sehingga tidak mengherankan jika terbunuh.
Peter Lawrey
di mana / bagaimana Anda mengatur konfigurasi -Xms dan -Xmx?
Mick
1
Apakah program Anda menjalankan banyak operasi file (misalnya membuat file dalam ukuran gigabyte)? Jika demikian, Anda harus tahu bahwa cgroupsmenambahkan disk-cache ke memori yang digunakan - bahkan jika itu ditangani oleh kernel dan itu tidak terlihat oleh program pengguna. (Ingat, perintahkan psdan docker statsjangan hitung disk-cache.)
Lorinczy Zsigmond

Jawaban:

207

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)

  1. Java Heap

    Bagian yang paling jelas. Di sinilah objek Java hidup. Heap membutuhkan -Xmxsejumlah memori.

  2. 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:+UseSerialGCdan -XX:+UseShenandoahGCmemiliki biaya overhead terkecil. G1 atau CMS dapat dengan mudah menggunakan sekitar 10% dari total ukuran heap.

  3. 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:-TieredCompilationuntuk mengurangi jumlah kode yang dikompilasi dan dengan demikian penggunaan Cache Kode.

  4. 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.

  5. 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).

  6. 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.

  7. 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 dalam Otherbagian output NMT (atau Internalsebelum JDK 11).

Jumlah memori langsung yang digunakan terlihat melalui JMX, misalnya di JConsole atau Java Mission Control:

BufferPool MBean

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>

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 yang dimuat oleh System.loadLibrarydapat 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 adalah ZipInputStreamatau DirectoryStream.

Agen JVMTI, khususnya, jdwpagen 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 mmappanggilan sistem) atau dengan menggunakan malloc- pengalokasi libc standar. Pada gilirannya, mallocmeminta potongan besar memori dari OS yang digunakan mmap, 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 biasa malloc, jadi beralih ke jemallocdapat 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.

Total memory = Heap + Code Cache + Metaspace + Symbol tables +
               Other JVM structures + Thread stacks +
               Direct buffers + Mapped files +
               Native Libraries + Malloc overhead + ...

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.

apangin
sumber
1
Apakah Strings tidak disimpan di heap sejak jdk7? ( bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931 ) - mungkin saya salah.
j-keck
5
@ j-keck Objek string ada di heap, tetapi hashtable (bucket dan entri dengan referensi dan kode hash) ada di memori off-heap. Saya mengubah kalimatnya menjadi lebih tepat. Terima kasih telah menunjukkannya.
apangin
untuk menambahkan ini, meskipun Anda menggunakan ByteBuffers non-langsung, JVM akan mengalokasikan buffer langsung sementara di memori native tanpa batasan memori yang diberlakukan. Cf. evanjones.ca/java-bytebuffer-leak.html
Cpt. Senkfuss
16

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 heap 1gb. Itu tidak memberi tahu JVM untuk membatasi seluruh penggunaan memori menjadi 1gb. 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 tumpukan Anda akan menjadi sekitar 250mb.

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:MaxRAMakan 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 Docker

Memori (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.

Jan Garaj
sumber
Memang lebih baik dengan -XX:MaxRam. Saya pikir ini masih menggunakan lebih dari yang ditentukan maksimum tetapi lebih baik, terima kasih!
Nicolas Henneaux
Mungkin Anda benar-benar membutuhkan lebih banyak memori untuk instance Java ini. Ada 15267 kelas, 56 utas.
Jan Garaj
1
Berikut lebih detailnya, argumen Java -Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC, hasil Docker 428.5MiB / 600MiBdan jcmd 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?
Nicolas Henneaux
1
Menambahkan info / link tentang memori RSS.
Jan Garaj
RSS yang disediakan berasal dari dalam wadah untuk proses Java hanya ps -p 71 -o pcpu,rss,size,vsizedengan proses Java yang memiliki pid 71. Sebenarnya -XX:MaxRamtidak membantu tetapi tautan yang Anda berikan membantu dengan serial GC.
Nicolas Henneaux
8

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).

-Xshare:off

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).

-XX:+UseSerialGC

The C2 compiler dapat dinonaktifkan dengan bendera berikut untuk mengurangi profil data yang digunakan untuk memutuskan apakah akan mengoptimalkan atau tidak metode.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

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.

-Xint

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.

Nicolas Henneaux
sumber
-1

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.

adiian
sumber
Penggunaannya jlinkcukup terbatas karena memerlukan aplikasi untuk menjadi modularis. Modul otomatis tidak didukung sehingga tidak ada cara mudah untuk pergi ke sana.
Nicolas Henneaux
-1

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

docker run -d --name my-container --memory 500m <iamge-name>

tidak bisa menjawab pertanyaan lain.

v_sukt
sumber