Yığın boyutundan çok daha fazla bellek kullanan Java (veya Docker bellek sınırını doğru bir şekilde boyutlandırın)


118

Benim uygulamam için, Java işlemi tarafından kullanılan bellek yığın boyutundan çok daha fazla.

Kapsayıcıların çalıştığı sistem bellek sorunu yaşamaya başlar çünkü kapsayıcı yığın boyutundan çok daha fazla bellek alır.

Yığın boyutu 128 MB ( -Xmx128m -Xms128m) olarak ayarlanırken , kapsayıcı 1 GB bellek alır. Normal şartlar altında 500MB'ye ihtiyacı vardır. Docker konteynerinin altında bir limit varsa (örneğin mem_limit=mem_limit=400MB), işlem işletim sisteminin yetersiz bellek katili tarafından öldürülür.

Java işleminin neden yığından çok daha fazla bellek kullandığını açıklayabilir misiniz? Docker bellek sınırı nasıl doğru boyutlandırılır? Java işleminin yığın dışı bellek ayak izini azaltmanın bir yolu var mı?


JVM'de Yerel bellek izleme komutunu kullanarak sorunla ilgili bazı ayrıntıları topluyorum .

Ana sistemden, konteyner tarafından kullanılan belleği alıyorum.

$ 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

Kabın içinden, işlem tarafından kullanılan belleği alıyorum.

$ 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

Uygulama Jetty / Jersey / CDI kullanan bir web sunucusudur.

Aşağıdaki işletim sistemi ve Java sürümü kullanılmaktadır (kapsayıcı içinde). Docker görüntüsü temel alır 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


6
Yığın, nesnelerin tahsis edildiği yerdir, ancak JVM, paylaşılan kitaplıklar, doğrudan bellek arabellekleri, iş parçacığı yığınları, GUI bileşenleri, meta alan dahil olmak üzere başka birçok bellek bölgesine sahiptir. JVM'nin ne kadar büyük olabileceğine bakmanız ve limiti, sürecin artık kullanmaktansa ölmesini tercih edecek kadar yüksek tutmanız gerekir.
Peter Lawrey

2
Görünüşe göre GC çok fazla bellek kullanıyor. Bunun yerine CMS toplayıcısını kullanmayı deneyebilirsiniz. Metaspace + kod için ~ 125 MB kullanılmış gibi görünüyor, ancak kod tabanınızı küçültmeden, bunu küçültme olasılığınız düşüktür. Belirlenen alan sınırınıza yakın, bu yüzden öldürülmesi şaşırtıcı değil.
Peter Lawrey

-Xms ve -Xmx yapılandırmasını nerede / nasıl ayarlarsınız?
Mick


1
Programınız birçok dosya işlemini yürütüyor mu (örn. Gigabayt boyutunda dosyalar oluşturur)? Eğer öyleyse, cgroupsçekirdek tarafından işleniyor ve kullanıcı programı için görünmez olsa bile, kullanılan belleğe disk önbelleği eklediğini bilmelisiniz . (Dikkat edin, komutlar psve docker statsdisk önbelleğini
saymayın

Yanıtlar:


207

Bir Java işlemi tarafından kullanılan sanal bellek, Java Heap'in çok ötesine geçer. Biliyorsunuz, JVM birçok alt sistem içerir: Çöp Toplayıcı, Sınıf Yükleme, JIT derleyicileri vb. Tüm bu alt sistemler çalışmak için belirli miktarda RAM gerektirir.

JVM, RAM'in tek tüketicisi değildir. Yerel kitaplıklar (standart Java Sınıf Kitaplığı dahil) ayrıca yerel bellek ayırabilir. Ve bu, Yerel Bellek İzleme tarafından görünmeyecek bile. Java uygulamasının kendisi de doğrudan ByteBuffers aracılığıyla yığın dışı bellek kullanabilir.

Peki bir Java sürecinde hafızayı ne alır?

JVM parçaları (çoğunlukla Yerel Bellek İzleme ile gösterilir)

  1. Java Yığını

    En bariz kısım. Burası Java nesnelerinin yaşadığı yerdir. Yığın -Xmxbellek miktarı kadar yer kaplar .

  2. Çöp toplayıcı

    GC yapıları ve algoritmaları, yığın yönetimi için ek bellek gerektirir. Bu yapılar Mark Bitmap, Mark Stack (nesne grafiğini geçmek için), Hatırlanan Kümeler (bölgeler arası referansları kaydetmek için) ve diğerleridir. Bazıları doğrudan ayarlanabilir, örneğin -XX:MarkStackSizeMaxdiğerleri yığın düzenine bağlıdır, örneğin daha büyük G1 bölgeleri ( -XX:G1HeapRegionSize), daha küçük hatırlanan kümelerdir.

    GC bellek ek yükü, GC algoritmaları arasında değişir. -XX:+UseSerialGCve -XX:+UseShenandoahGCen küçük ek yüke sahip. G1 veya CMS, toplam yığın boyutunun yaklaşık% 10'unu kolayca kullanabilir.

  3. Kod Önbelleği

    Dinamik olarak oluşturulmuş kod içerir: JIT ile derlenmiş yöntemler, yorumlayıcı ve çalışma zamanı saplamaları. Boyutu -XX:ReservedCodeCacheSize(varsayılan olarak 240M) ile sınırlıdır . -XX:-TieredCompilationDerlenen kod miktarını ve dolayısıyla Kod Önbelleği kullanımını azaltmak için kapatın .

  4. Derleyici

    JIT derleyicisinin kendisi de işini yapmak için belleğe ihtiyaç duyar. Bu Katmanlı Compilation kapatarak veya derleyici parçacığı sayısını azaltarak yeniden azaltılabilir: -XX:CICompilerCount.

  5. Sınıf yükleme

    Sınıf meta verileri (yöntem bayt kodları, semboller, sabit havuzlar, ek açıklamalar vb.) Metaspace adı verilen yığın dışı alanda depolanır. Ne kadar çok sınıf yüklenirse, o kadar çok meta alanı kullanılır. Toplam kullanım -XX:MaxMetaspaceSize(varsayılan olarak sınırsız) ve -XX:CompressedClassSpaceSize(varsayılan olarak 1G ) ile sınırlandırılabilir .

  6. Sembol tabloları

    JVM'nin iki ana hashtable'ı: Symbol tablosu isimler, imzalar, tanımlayıcılar vb. İçerir ve String tablosu, dahili dizelere referanslar içerir. Yerel Bellek İzleme, bir String tablosu tarafından önemli bellek kullanımını gösteriyorsa, bu muhtemelen uygulamanın aşırı derecede çağırdığı anlamına gelir String.intern.

  7. İş Parçacığı

    İplik yığınları da RAM almaktan sorumludur. Yığın boyutu tarafından kontrol edilir -Xss. Varsayılan değer, iş parçacığı başına 1M'dir, ancak neyse ki işler o kadar da kötü değil. İşletim sistemi bellek sayfalarını tembel olarak ayırır, yani ilk kullanımda, bu nedenle gerçek bellek kullanımı çok daha düşük olacaktır (tipik olarak iş parçacığı yığını başına 80-200 KB). RSS'nin ne kadarının Java iş parçacığı yığınlarına ait olduğunu tahmin etmek için bir komut dosyası yazdım .

    Yerel belleği ayıran başka JVM parçaları da vardır, ancak bunlar genellikle toplam bellek tüketiminde büyük bir rol oynamazlar.

Doğrudan tamponlar

Bir uygulama, çağırarak açıkça yığın dışı bellek isteyebilir ByteBuffer.allocateDirect. Varsayılan yığın dışı sınırı eşittir -Xmx, ancak ile geçersiz kılınabilir -XX:MaxDirectMemorySize. Doğrudan ByteBuffers, OtherNMT çıktısının bölümüne (veya InternalJDK 11'den önce) dahil edilmiştir.

Kullanılan doğrudan bellek miktarı JMX aracılığıyla görülebilir, örneğin JConsole veya Java Mission Control:

BufferPool MBean

Doğrudan ByteBuffers'ın yanı sıra MappedByteBuffers, bir işlemin sanal belleğine eşlenen dosyalar olabilir . NMT bunları izlemez, ancak MappedByteBuffers ayrıca fiziksel bellek de alabilir. Ve ne kadar dayanabileceklerini sınırlamanın basit bir yolu yok. İşlem bellek haritasına bakarak sadece gerçek kullanımı görebilirsiniz: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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

Yerel kitaplıklar

Tarafından yüklenen JNI kodu System.loadLibrary, JVM tarafından kontrol olmaksızın istediği kadar yığın dışı bellek ayırabilir. Bu aynı zamanda standart Java Sınıf Kitaplığı ile ilgilidir. Özellikle, kapatılmamış Java kaynakları yerel bellek sızıntısının kaynağı olabilir. Tipik örnekler ZipInputStreamveya DirectoryStream.

JVMTI aracıları, özellikle jdwphata ayıklama aracısı da aşırı bellek tüketimine neden olabilir.

Bu cevap , zaman uyumsuz profil oluşturucu ile yerel bellek ayırmalarının nasıl profilleneceğini açıklar .

Ayırıcı sorunları

Bir işlem tipik olarak yerel belleği ya doğrudan işletim sisteminden ( mmapsistem çağrısı ile) ya da malloc- standart libc ayırıcı kullanarak ister . Buna karşılık, mallocişletim sisteminden büyük bellek parçaları talep eder mmapve ardından bu yığınları kendi ayırma algoritmasına göre yönetir. Sorun şu ki, bu algoritma parçalanmaya ve aşırı sanal bellek kullanımına neden olabilir .

jemalloc, alternatif bir ayırıcı, genellikle normal libc'den daha akıllı görünür malloc, bu nedenle geçiş yapmak jemallocücretsiz olarak daha küçük bir alan kaplamasına neden olabilir.

Sonuç

Bir Java işleminin tam bellek kullanımını tahmin etmenin garantili bir yolu yoktur, çünkü dikkate alınması gereken çok fazla faktör vardır.

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

JVM bayrakları ile belirli bellek alanlarını (Kod Önbelleği gibi) küçültmek veya sınırlamak mümkündür, ancak diğerlerinin çoğu JVM kontrolünün dışında kalmıştır.

Docker sınırlarını belirlemeye yönelik olası bir yaklaşım, işlemin "normal" durumunda gerçek bellek kullanımını izlemek olacaktır. : Java bellek tüketimi ile ilgili sorunları araştırmak için araç ve teknikler vardır Yerli Bellek İzleme , pmap , jemalloc , async-profil .

Güncelleme

İşte bir Java İşleminin Bellek Ayak İzi sunumumun kaydı .

Bu videoda, bir Java işleminde belleği neyin tüketebileceğini, belirli bellek alanlarının boyutunun nasıl izlenip sınırlandırılacağını ve bir Java uygulamasında yerel bellek sızıntılarının nasıl profilleneceğini tartışıyorum.


1
Jdk7'den bu yana yığında tutulan Dizeler değil mi? ( bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931 ) - belki yanılıyorum.
j-keck

5
@ j-keck Dize nesneleri yığın içindedir, ancak hashtable (kovalar ve referanslar ve karma kodlu girişler) yığın dışı bellekte bulunur. Daha kesin olması için cümleyi yeniden yazdım. Gösterdiğiniz için teşekkürler.
apangin

buna ek olarak, doğrudan olmayan ByteBuffers kullansanız bile, JVM, herhangi bir bellek sınırı olmaksızın yerel bellekte geçici doğrudan arabellekleri tahsis edecektir. Krş evanjones.ca/java-bytebuffer-leak.html
Cpt. Senkfuss

16

https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/ :

-Xmx = 1g belirttiğimde neden JVM'm 1gb bellekten daha fazla bellek kullanıyor?

-Xmx = 1g belirtilmesi, JVM'ye 1gb'lik bir yığın ayırmasını bildirir. JVM'ye tüm bellek kullanımını 1 gb ile sınırlamasını söylemiyor. Kart tabloları, kod önbellekleri ve her türlü diğer yığın dışı veri yapıları vardır. Toplam bellek kullanımını belirtmek için kullandığınız parametre -XX: MaxRAM'dir. -XX: MaxRam = 500m ile yığınınızın yaklaşık 250mb olacağını unutmayın.

Java, ana bilgisayar bellek boyutunu görür ve herhangi bir kapsayıcı bellek sınırlamasının farkında değildir. Bellek baskısı oluşturmaz, bu nedenle GC'nin kullanılan belleği serbest bırakması gerekmez. Umarım XX:MaxRAMbellek ayak izini azaltmanıza yardımcı olur. Sonunda, GC yapılandırma tweak ( -XX:MinHeapFreeRatio, -XX:MaxHeapFreeRatio, ...)


Birçok bellek ölçüsü türü vardır. Docker, RSS bellek boyutunu bildiriyor gibi görünüyor, bu, tarafından bildirilen "kaydedilmiş" bellekten farklı olabilir jcmd(Docker'ın eski sürümleri, RSS + önbelleği bellek kullanımı olarak bildirir). İyi tartışma ve bağlantılar: Docker konteynerinde çalışan bir JVM için Yerleşik Küme Boyutu (RSS) ile Java toplam kaydedilmiş bellek (NMT) arasındaki fark

(RSS) belleği, kapsayıcıdaki diğer bazı yardımcı programlar tarafından da tüketilebilir - kabuk, işlem yöneticisi, ... Kapta başka ne çalıştığını ve kapsayıcıda işlemleri nasıl başlattığınızı bilmiyoruz.


Gerçekten daha iyi -XX:MaxRam. Sanırım hala tanımlanan maksimumdan daha fazlasını kullanıyor ama daha iyi, teşekkürler!
Nicolas Henneaux

Belki bu Java örneği için gerçekten daha fazla belleğe ihtiyacınız vardır. 15267 sınıf, 56 iş parçacığı vardır.
Jan Garaj

1
İşte daha fazla ayrıntı, Java argümanları -Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC, üretir Docker 428.5MiB / 600MiBve jcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KB. JVM, konteynerin 430MB'a ihtiyacı varken yaklaşık 300MB alıyor. JVM raporlaması ile işletim sistemi raporlaması arasındaki 130 MB nerede?
Nicolas Henneaux

1
RSS belleği hakkında bilgi / bağlantı eklendi.
Jan Garaj

Sağlanan RSS, yalnızca ps -p 71 -o pcpu,rss,size,vsizepid 71'e sahip Java işlemi ile Java işlemi için konteynerin içindendir. Aslında -XX:MaxRamyardımcı olmuyordu ancak sağladığınız bağlantı, seri GC'ye yardımcı oluyor.
Nicolas Henneaux

8

TL; DR

Belleğin ayrıntılı kullanımı Yerel Bellek İzleme (NMT) ayrıntıları (çoğunlukla kod meta verileri ve çöp toplayıcı) tarafından sağlanır. Buna ek olarak, Java derleyicisi ve iyileştirici C1 / C2, özette bildirilmeyen belleği kullanır.

Bellek ayak izi JVM bayrakları kullanılarak azaltılabilir (ancak etkiler vardır).

Docker kapsayıcı boyutlandırması, uygulamanın beklenen yükü ile test edilerek yapılmalıdır.


Her bileşen için ayrıntı

Paylaşılan sınıf uzay sınıfları başka JVM işlem tarafından paylaşılmaz beri bir konteynerin içinde devre dışı bırakılabilir. Aşağıdaki bayrak kullanılabilir. Paylaşılan sınıf alanını (17MB) kaldıracaktır.

-Xshare:off

Çöp toplayıcı seri çöp toplama işlemi sırasında daha uzun duraklama süresi pahasına en az bir bellek alanına (bkz sahip bir resim GC arasında Aleksey Shipilëv karşılaştırma ). Aşağıdaki bayrakla etkinleştirilebilir. Kullanılan GC alanına (48MB) kadar tasarruf edebilir.

-XX:+UseSerialGC

C2 derleyici bir yöntem optimize etmek ya da karar vermek için kullanılan profil verileri azaltmak üzere aşağıdaki bayrak ile devre dışı bırakılabilir.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

Kod alanı 20MB azaltılır. Ayrıca, JVM dışındaki bellek 80MB (NMT alanı ile RSS alanı arasındaki fark) azaltılır. Optimize edici derleyici C2'nin 100MB'ye ihtiyacı vardır.

C1 ve C2 derleyici aşağıdaki bayrak ile devre dışı bırakılabilir.

-Xint

JVM dışındaki bellek artık toplam kaydedilmiş alandan daha düşüktür. Kod alanı 43MB azaltılır. Dikkat edin, bunun uygulamanın performansı üzerinde büyük bir etkisi vardır.C1 ve C2 derleyicisini devre dışı bırakmak, kullanılan belleği 170 MB azaltır.

Kullanma Graal VM derleyici biraz daha küçük bir bellek alanı için (C2 değiştirilmesi) potansiyel. Kod bellek alanını 20MB artırır ve JVM belleğinin dışından 60MB azalır.

JVM için Java Bellek Yönetimi makalesi , farklı bellek alanlarıyla ilgili bazı bilgiler sağlar. Oracle, Yerel Bellek İzleme belgelerinde bazı ayrıntılar sağlar . Gelişmiş derleme ilkesinde ve devre dışı bırakma C2'de derleme düzeyi hakkında daha fazla ayrıntı, kod önbelleği boyutunu bir faktör 5 azaltır . Bazı ayrıntılar Why Linux daha JVM raporu daha bağlı bellek yerleşik kümesinin boyutunu işliyor? her iki derleyici de devre dışı bırakıldığında.


-1

Java'nın çok fazla belleğe ihtiyacı var. JVM'nin çalışması için çok fazla belleğe ihtiyacı vardır. Yığın, uygulamanızın kullanabileceği, sanal makinenin içinde bulunan bellektir. JVM, mümkün olan tüm güzelliklerle dolu büyük bir paket olduğundan, yalnızca yüklenmesi çok fazla bellek gerektirir.

Java 9'dan başlayarak, bir java uygulamasını başlattığınızda (başlangıç ​​zamanıyla birlikte) kullanılan belleği azaltabilecek Jigsaw projesi adlı bir şeye sahipsiniz. Proje yapbozu ve yeni bir modül sistemi, gerekli belleği azaltmak için mutlaka oluşturulmamıştı, ancak önemliyse bir deneyebilirsiniz.

Bu örneğe bir göz atabilirsiniz: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/ . Modül sistemini kullanarak 21MB'lık CLI uygulamasıyla sonuçlandı (JRE gömülü). JRE, 200mb'den fazla yer kaplıyor. Bu, uygulama açıkken daha az ayrılmış belleğe dönüştürülmelidir (kullanılmayan birçok JRE sınıfı artık yüklenmeyecektir).

İşte başka bir güzel öğretici: https://www.baeldung.com/project-jigsaw-java-modularity

Bununla zaman geçirmek istemiyorsanız, daha fazla bellek ayırabilirsiniz. Bazen en iyisidir.


Kullanılması jlinko modüler bir yapıya edilecek uygulamayı gerektiği gibi oldukça kısıtlayıcı. Otomatik modül desteklenmediğinden oraya gitmenin kolay bir yolu yoktur.
Nicolas Henneaux

-1

Docker bellek sınırı nasıl doğru boyutlandırılır? Bir süre izleyerek uygulamayı kontrol edin. Kapsayıcının belleğini kısıtlamak için docker run komutu için -m, --memory bytes seçeneğini kullanmayı deneyin - veya başka türlü çalıştırıyorsanız eşdeğer bir şey

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

diğer soruları cevaplayamıyorum.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.