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.