Natív memóriakövetés a JVM-ben

1. Áttekintés

Elgondolkozott már azon, vajon a Java-alkalmazások miért fogyasztanak sokkal több memóriát, mint a közismerten megadott mennyiség -Xms és -Xmx zászlók tuningolása? Különböző okok és lehetséges optimalizálások miatt a JVM külön natív memóriát rendelhet hozzá. Ezek az extra allokációk végül az elfogyasztott memóriát növelhetik a -Xmx korlátozás.

Ebben az oktatóanyagban felsoroljuk a JVM natív memória-allokációinak néhány gyakori forrását, azok méretezési tuningjaival együtt, majd megtanuljuk, hogyan kell használni Natív memóriakövetés hogy figyelemmel kísérje őket.

2. Natív allokációk

A halom általában a legnagyobb memóriafogyasztó a Java alkalmazásokban, de vannak mások is. A halom mellett a JVM meglehetősen nagy darabot oszt ki a natív memóriából osztály-metaadatok, alkalmazáskód, a JIT által generált kód, belső adatszerkezetek stb. Fenntartására. A következő szakaszokban ezeknek a kiosztásoknak néhányat vizsgálunk meg.

2.1. Metaspace

A betöltött osztályok néhány metaadatának fenntartása érdekében a JVM egy dedikált, nem kupacos területet használ Metaspace. A Java 8 előtt az egyenértéket hívták meg PermGen vagy Állandó Generáció. A Metaspace vagy a PermGen a betöltött osztályokról tartalmaz metaadatokat, nem pedig azok példányait, amelyeket a kupacban tartanak.

A fontos itt az a halom méretezési konfigurációk nem befolyásolják a Metaspace méretét mivel a Metaspace halom nélküli adatterület. A Metaspace méretének korlátozása érdekében más tuning zászlókat használunk:

  • -XX: MetaspaceSize és -XX: MaxMetaspaceSize a minimális és maximális Metaspace méret beállításához
  • Java 8 előtt -XX: PermSize és -XX: MaxPermSize a minimális és maximális PermGen méret beállításához

2.2. Szálak

A JVM egyik legtöbb memóriát felemésztő adatterülete a verem, amelyet minden szálral egyidejűleg hoztak létre. A verem a helyi változókat és a részeredményeket tárolja, fontos szerepet játszik a módszer meghívásában.

Az alapértelmezett szálverem mérete platformfüggő, de a legtöbb modern 64 bites operációs rendszerben ez körülbelül 1 MB. Ez a méret a -Xss tuning zászló.

A többi adatterülettel ellentétben a halmokhoz rendelt teljes memória gyakorlatilag korlátlan, ha nincs korlátozás a szálak számára. Érdemes megemlíteni azt is, hogy a JVM-nek is szüksége van néhány szálra belső műveleteinek végrehajtásához, például a GC-hez vagy az éppen időben történő összeállításokhoz.

2.3. Kód gyorsítótár

A JVM bytecode futtatásához különböző platformokon konvertálni kell gépi utasításokká. A JIT fordító felelős a fordításért a program végrehajtásakor.

Amikor a JVM bájtkódot fordít az összeszerelési utasításokhoz, ezeket az utasításokat egy speciális, nem kupacos adatterületen tárolja. Kód gyorsítótár. A kód-gyorsítótár ugyanúgy kezelhető, mint a JVM többi adatterülete. A -XX: InitialCodeCacheSize és -XX: ReservedCodeCacheSize a tuning zászlók meghatározzák a kód gyorsítótárának kezdeti és maximális méretét.

2.4. Szemétgyüjtés

A JVM-hez egy maroknyi GC algoritmust szállítanak, amelyek mindegyike különböző felhasználási esetekre alkalmas. Ezeknek a GC algoritmusoknak egy közös vonása van: feladataik elvégzéséhez néhány halom adatszerkezetet kell használniuk. Ezek a belső adatszerkezetek több natív memóriát emésztenek fel.

2.5. Szimbólumok

Kezdjük azzal Húrok, az alkalmazás- és könyvtárkódban az egyik leggyakrabban használt adattípus. Mindenütt jelenlétük miatt általában a Halom nagy részét elfoglalják. Ha ezeknek a húroknak a nagy része ugyanazt a tartalmat tartalmazza, akkor a kupac jelentős része el fog pazarolni.

Halomnyi hely megtakarítása érdekében mindegyikből egy-egy verziót tárolhatunk Húr és tegye másokat a tárolt verzióra. Ezt a folyamatot hívják Vonós internálás.Mivel a JVM csak gyakornok lehet Idő húrállandók összeállítása, manuálisan hívhatjuk a gyakornok() módszer a húrokon kívánunk internálni.

A JVM az internált húrokat egy speciális natív, rögzített méretű hashtable-ben tárolja Vonós asztal, más néven Vonós medence. A táblázat méretét (vagyis a vödrök számát) a -XX: StringTableSize tuning zászló.

A karakterlánc táblán kívül van még egy natív adatterület, az úgynevezett Futásidejű állandó medence. A JVM ezt a készletet olyan konstansok tárolására használja, mint fordítási idejű numerikus literálok vagy módszer és mező hivatkozások, amelyeket futás közben kell megoldani.

2.6. Natív bájtpufferek

A JVM a szokásos gyanúsított jelentős számú natív kiosztás esetén, de néha a fejlesztők közvetlenül kioszthatják a natív memóriát is. A leggyakoribb megközelítések a malloc a JNI és a NIO közvetlen felhívása ByteBuffers.

2.7. További tuning zászlók

Ebben a szakaszban egy maroknyi JVM tuning zászlót használtunk különböző optimalizálási forgatókönyvekhez. A következő tipp segítségével szinte az összes hangolási zászlót megtalálhatjuk, amelyek egy adott koncepcióhoz kapcsolódnak:

$ java -XX: + PrintFlagsFinal -version | grep 

A PrintFlagsFinal kinyomtatja az összes -XX opciók a JVM-ben. Például az összes Metaspace-hez kapcsolódó zászló megkereséséhez:

$ java -XX: + PrintFlagsFinal -version | grep Metaspace // csonka uintx MaxMetaspaceSize = 18446744073709547520 {product} uintx MetaspaceSize = 21807104 {pd product} // csonka

3. Natív memóriakövetés (NMT)

Most, hogy ismerjük a natív memória-allokációk közös forrásait a JVM-ben, itt az ideje, hogy megtudja, hogyan figyelheti őket. Először engedélyeznünk kell a natív memória követését egy újabb JVM tuning zászló használatával: -XX: NativeMemoryTracking = ki | összefoglaló | részlet. Alapértelmezés szerint az NMT ki van kapcsolva, de lehetővé tehetjük számára a megfigyelések összefoglaló vagy részletes nézetének megtekintését.

Tegyük fel, hogy egy tipikus Spring Boot alkalmazás natív allokációit szeretnénk nyomon követni:

$ java -XX: NativeMemoryTracking = összefoglaló -Xms300m -Xmx300m -XX: + UseG1GC -jar app.jar

Itt engedélyezzük az NMT-t, miközben 300 MB halomterületet allokálunk, a G1-vel a GC algoritmusként.

3.1. Azonnali pillanatképek

Ha az NMT engedélyezve van, akkor a natív memória információkat bármikor megszerezhetjük a jcmd parancs:

$ jcmd VM.native_memory

A JVM alkalmazás PID-jének megkereséséhez használhatjuk a jpsparancs:

$ jps -l 7858 app.jar // Ez a mi 7899-es alkalmazásunk sun.tools.jps.Jps

Most, ha használjuk jcmd a megfelelővel pid, a VM.honos_memória a JVM-et kinyomtatja a natív kiosztásokkal kapcsolatos információkat:

$ jcmd 7858 VM.honos_memória

Elemezzük az NMT kimenetét szakaszonként.

3.2. Összes kiosztás

Az NMT a teljes lefoglalt és lekötött memóriát a következőképpen jelenti:

Natív memóriakövetés: Összesen: fenntartva = 1731124KB, lekötve = 448152KB

A lefoglalt memória az alkalmazásunk által esetlegesen felhasználható memória teljes mennyiségét jelenti. Ezzel szemben a lekötött memória megegyezik az alkalmazásunk által jelenleg használt memória mennyiségével.

A 300 MB halom kiosztása ellenére az alkalmazásunk számára fenntartott teljes memória majdnem 1,7 GB, ennél sokkal több. Hasonlóképpen, a lekötött memória 440 MB körül van, ami megint sokkal több, mint 300 MB.

A teljes szakasz után az NMT allokációs forrásonként jelent memóriaallokációt. Tehát vizsgáljuk meg alaposan az egyes forrásokat.

3.3. Halom

Az NMT a halom kiosztásunkról számol be, amire számítottunk:

Java halom (fenntartva = 307200KB, lekötve = 307200KB) (mmap: fenntartva = 307200KB, elkötelezett = 307200KB)

300 MB lefoglalt és lekötött memória, amely megfelel a kupacméret-beállításunknak.

3.4. Metaspace

Az NMT a következőket mondja a betöltött osztályok metaadatairól:

Osztály (fenntartva = 1091407KB, lekötve = 45815KB) (6566-os osztályok) (malloc = 10063KB # 8519) (mmap: fenntartva = 1081344KB, lekötött = 35752KB)

Szinte 1 GB van fenntartva és 45 MB van elkötelezve 6566 osztály betöltésére.

3.5. cérna

És itt van az NMT jelentése a szálallokációkról:

Szál (fenntartva = 37018KB, lekötve = 37018KB) (37. szál) (verem: fenntartva = 36864KB, lekötve = 36864KB) (malloc = 112KB # 190) (aréna = 42KB # 72)

Összesen 36 MB memória van allokálva 37 szál veremhez - csaknem 1 MB veremenként. A JVM a létrehozáskor szálakhoz rendeli a memóriát, így a fenntartott és a lekötött allokáció megegyezik.

3.6. Kód gyorsítótár

Lássuk, mit mond az NMT a JIT által létrehozott és gyorsítótárazott szerelési utasításokról:

Kód (fenntartva = 251549KB, lekötve = 14169KB) (malloc = 1949KB # 3424) (mmap: fenntartva = 249600KB, lekötve = 12220KB)

Jelenleg csaknem 13 MB kód van gyorsítótárban, és ez az összeg potenciálisan akár 245 MB is lehet.

3.7. GC

Íme az NMT jelentése a G1 GC memóriahasználatáról:

GC (fenntartva = 61771KB, lekötve = 61771KB) (malloc = 17603KB # 4501) (mmap: fenntartva = 44168KB, lekötve = 44168KB)

Mint láthatjuk, csaknem 60 MB van fenntartva és elkötelezett a G1 segítése mellett.

Lássuk, hogyan néz ki a memóriahasználat egy sokkal egyszerűbb GC esetében, mondjuk a Serial GC esetében:

$ java -XX: NativeMemoryTracking = összefoglaló -Xms300m -Xmx300m -XX: + UseSerialGC -jar app.jar

A Serial GC alig használ 1 MB-ot:

GC (fenntartva = 1034 KB, lekötve = 1034 KB) (malloc = 26 KB # 158) (mmap: fenntartva = 1008 KB, lekötve = 1008 KB)

Nyilvánvaló, hogy csak a memóriahasználata miatt nem szabad választanunk egy GC algoritmust, mivel a soros GC világmegállási jellege teljesítményromlást okozhat. Ugyanakkor több GC közül lehet választani, és mindegyik másképp egyensúlyozza a memóriát és a teljesítményt.

3.8. Szimbólum

Itt található az NMT jelentése a szimbólumok kiosztásáról, például a karakterlánc tábláról és az állandó készletről:

Szimbólum (fenntartva = 10148KB, lekötött = 10148KB) (malloc = 7295KB # 66194) (aréna = 2853KB # 1)

Csaknem 10 MB van elosztva a szimbólumok számára.

3.9. NMT idővel

Az NMT lehetővé teszi számunkra annak nyomon követését, hogy a memóriafoglalások hogyan változnak az idő múlásával. Először is ki kell jelölnünk alkalmazásunk jelenlegi állapotát alapként:

$ jcmd VM.native_memory baseline Sikeres

Ezután egy idő után összehasonlíthatjuk a jelenlegi memóriahasználatot azzal az alapvonallal:

$ jcmd VM.native_memory összefoglaló.diff

Az NMT a + és - jelek használatával elmondaná, hogyan változott a memóriahasználat ebben az időszakban:

Összesen: fenntartva = 1771487KB + 3373KB, lekötve = 491491KB + 6873KB - Java Heap (fenntartva = 307200KB, lekötve = 307200KB) (mmap: fenntartva = 307200KB, lekötve = 307200KB) - Osztály (fenntartva = 1084300KB + 2103KB, lekötve = 39356KB + 2871KB ) // Csonka

A teljes lefoglalt és lekötött memória 3, illetve 6 MB-mal nőtt. A memóriafoglalások egyéb ingadozásai ugyanolyan könnyen észlelhetők.

3.10. Részletes NMT

Az NMT nagyon részletes információkat tud nyújtani a teljes memóriaterület térképéről. A részletes jelentés engedélyezéséhez a -XX: NativeMemoryTracking = részlet tuning zászló.

4. Következtetés

Ebben a cikkben különféle közreműködőket soroltunk fel a natív memória-allokációkhoz a JVM-ben. Ezután megtanultuk, hogyan kell ellenőrizni egy futó alkalmazást a natív allokációk figyelemmel kísérése érdekében. Ezekkel a felismerésekkel hatékonyabban hangolhatjuk alkalmazásainkat és méretezhetjük futási környezetünket.