Native Memory Tracking i JVM

1. Oversigt

Nogensinde spekuleret på, hvorfor Java-applikationer bruger meget mere hukommelse end det angivne beløb via det velkendte -Xms og -Xmx tuning flag? Af forskellige årsager og mulige optimeringer tildeler JVM muligvis ekstra indbygget hukommelse. Disse ekstra tildelinger kan i sidste ende hæve den forbrugte hukommelse ud over -Xmx begrænsning.

I denne vejledning vil vi opregne et par almindelige kilder til indbygget hukommelsesallokering i JVM sammen med deres størrelsesindstillingsflag og derefter lære at bruge Native Memory Tracking for at overvåge dem.

2. Indfødte tildelinger

Bunken er normalt den største forbruger af hukommelse i Java-applikationer, men der er andre. Udover bunken tildeler JVM et ret stort stykke fra den oprindelige hukommelse for at opretholde sin klassemetadata, applikationskode, koden genereret af JIT, interne datastrukturer osv. I de følgende afsnit undersøger vi nogle af disse tildelinger.

2.1. Metaspace

For at opretholde nogle metadata om de indlæste klasser bruger JVM et dedikeret ikke-bunkeområde kaldet Metaspace. Før Java 8 blev ækvivalenten kaldt PermGen eller Permanent generation. Metaspace eller PermGen indeholder metadataene om de indlæste klasser snarere end forekomsterne af dem, som holdes inde i bunken.

Det vigtige her er, at konfigurationerne af heapstørrelse påvirker ikke Metaspace-størrelsen da Metaspace er et dataområde uden for bunken. For at begrænse Metaspace-størrelsen bruger vi andre tuningflag:

  • -XX: MetaspaceSize og -XX: MaxMetaspaceSize for at indstille den minimale og maksimale Metaspace-størrelse
  • Før Java 8, -XX: PermSize og -XX: MaxPermSize for at indstille den mindste og maksimale PermGen-størrelse

2.2. Tråde

Et af de mest hukommelseskrævende dataområder i JVM er stakken, der oprettes på samme tid som hver tråd. Stakken gemmer lokale variabler og delvise resultater og spiller en vigtig rolle i metodeopkald.

Standardstabelstørrelsen på tråd er platformafhængig, men i de fleste moderne 64-bit operativsystemer er den omkring 1 MB. Denne størrelse kan konfigureres via -Xss tuning flag.

I modsætning til andre dataområder den samlede hukommelse allokeret til stakke er praktisk talt ubegrænset, når der ikke er nogen begrænsning på antallet af tråde. Det er også værd at nævne, at JVM selv har brug for et par tråde for at udføre sine interne operationer som GC eller just-in-time kompileringer.

2.3. Kode cache

For at køre JVM bytecode på forskellige platforme skal den konverteres til maskininstruktioner. JIT-kompilatoren er ansvarlig for denne kompilering, når programmet udføres.

Når JVM kompilerer bytekode til monteringsinstruktioner, gemmer den instruktionerne i et specielt ikke-bunke dataområde kaldet Kode cache. Kodecachen kan administreres ligesom andre dataområder i JVM. Det -XX: InitialCodeCacheSize og -XX: ReservedCodeCacheSize tuning flag bestemmer den indledende og maksimale mulige størrelse for kode cache.

2.4. Dagrenovation

JVM leveres med en håndfuld GC-algoritmer, der hver er velegnede til forskellige brugssager. Alle disse GC-algoritmer deler et fælles træk: de skal bruge nogle off-heap datastrukturer til at udføre deres opgaver. Disse interne datastrukturer bruger mere indbygget hukommelse.

2.5. Symboler

Lad os starte med Strenge, en af ​​de mest anvendte datatyper i applikation og bibliotekode. På grund af deres allestedsnærværende besætter de normalt en stor del af bunken. Hvis et stort antal af disse strenge indeholder det samme indhold, bliver en væsentlig del af dyngen spildt.

For at spare lidt bunkeplads kan vi gemme en version af hver Snor og få andre til at henvise til den gemte version. Denne proces kaldes String Interning.Da JVM kun kan praktikere Kompilér tidsstrengkonstanter, vi kan manuelt kalde praktikant () metode på strenge, vi agter at praktisere.

JVM gemmer internerede strenge i en speciel indbygget fast størrelse hashtable kaldet String Table, også kendt som String Pool. Vi kan konfigurere bordstørrelsen (dvs. antallet af spande) via -XX: StringTableSize tuning flag.

Ud over strengtabellen er der et andet oprindeligt dataområde kaldet Runtime konstant pool. JVM bruger denne pool til at gemme konstanter som numeriske bogstaver til kompileringstid eller metode- og feltreferencer, der skal løses ved kørsel.

2.6. Indfødte bytebuffere

JVM er den sædvanlige mistænkte for et betydeligt antal indfødte tildelinger, men nogle gange kan udviklere også allokere direkte hukommelse. De mest almindelige tilgange er malloc opkald fra JNI og NIO's direkte ByteBuffers.

2.7. Yderligere tuningflagg

I dette afsnit brugte vi en håndfuld JVM-tuningflag til forskellige optimeringsscenarier. Ved hjælp af følgende tip kan vi finde næsten alle tuningflag relateret til et bestemt koncept:

$ java -XX: + PrintFlagsFinal -version | grep 

Det PrintFlagsFinal udskriver alle -XX indstillinger i JVM. For eksempel for at finde alle Metaspace-relaterede flag:

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

3. Native Memory Tracking (NMT)

Nu hvor vi kender de almindelige kilder til indbygget hukommelsesallokering i JVM, er det tid til at finde ud af, hvordan man overvåger dem. Først skal vi aktivere den oprindelige hukommelsessporing ved hjælp af endnu et JVM-tuningflag: -XX: NativeMemoryTracking = off | sumary | detail. Som standard er NMT slået fra, men vi kan sætte den i stand til at se et resumé eller detaljeret billede af dets observationer.

Lad os antage, at vi vil spore native-tildelinger til en typisk Spring Boot-applikation:

$ java -XX: NativeMemoryTracking = oversigt -Xms300m -Xmx300m -XX: + UseG1GC -jar app.jar

Her aktiverer vi NMT, samtidig med at vi tildeler 300 MB bunkeplads med G1 som vores GC-algoritme.

3.1. Øjeblikkelige snapshots

Når NMT er aktiveret, kan vi til enhver tid hente de oprindelige hukommelsesoplysninger ved hjælp af jcmd kommando:

$ jcmd VM.native_memory

For at finde PID til en JVM-applikation kan vi bruge jpskommando:

$ jps -l 7858 app.jar // Dette er vores app 7899 sun.tools.jps.Jps

Nu hvis vi bruger jcmd med det relevante pid, det VM.native_memory får JVM til at udskrive oplysningerne om indfødte tildelinger:

$ jcmd 7858 VM.native_memory

Lad os analysere NMT-output sektion for sektion.

3.2. Samlede tildelinger

NMT rapporterer den samlede reserverede og dedikerede hukommelse som følger:

Indbygget hukommelsessporing: I alt: reserveret = 1731124KB, begået = 448152KB

Reserveret hukommelse repræsenterer den samlede mængde hukommelse, som vores app potentielt kan bruge. Omvendt er den dedikerede hukommelse lig med den mængde hukommelse, som vores app bruger lige nu.

Trods tildeling af 300 MB bunke er den samlede reserverede hukommelse til vores app næsten 1,7 GB, meget mere end det. Tilsvarende er den dedikerede hukommelse omkring 440 MB, hvilket igen er meget mere end 300 MB.

Efter det samlede afsnit rapporterer NMT hukommelsesallokeringer pr. Allokeringskilde. Så lad os udforske hver kilde i dybden.

3.3. Bunke

NMT rapporterer vores dyngetildelinger som forventet:

Java Heap (reserveret = 307200KB, begået = 307200KB) (mmap: reserveret = 307200KB, begået = 307200KB)

300 MB både reserveret og dedikeret hukommelse, der matcher vores bunkestørrelsesindstillinger.

3.4. Metaspace

Her er hvad NMT siger om klassemetadataene for indlæste klasser:

Klasse (reserveret = 1091407KB, begået = 45815KB) (klasser # 6566) (malloc = 10063KB # 8519) (mmap: reserveret = 1081344KB, begået = 35752KB)

Næsten 1 GB reserveret og 45 MB forpligtet til at indlæse 6566 klasser.

3.5. Tråd

Og her er NMT-rapporten om trådallokeringer:

Tråd (reserveret = 37018KB, begået = 37018KB) (tråd nr.37) (stak: reserveret = 36864KB, begået = 36864KB) (malloc = 112KB # 190) (arena = 42KB # 72)

I alt tildeles 36 MB hukommelse til stakke til 37 tråde - næsten 1 MB pr. Stak. JVM tildeler hukommelsen til tråde på tidspunktet for oprettelsen, så de reserverede og forpligtede tildelinger er ens.

3.6. Kode cache

Lad os se, hvad NMT siger om de genererede og cachede monteringsvejledninger fra JIT:

Kode (reserveret = 251549KB, begået = 14169KB) (malloc = 1949KB # 3424) (mmap: reserveret = 249600KB, begået = 12220KB)

I øjeblikket caches næsten 13 MB kode, og dette beløb kan potentielt gå op til ca. 245 MB.

3.7. GC

Her er NMT-rapporten om G1 GC's hukommelsesforbrug:

GC (reserveret = 61771KB, begået = 61771KB) (malloc = 17603KB # 4501) (mmap: reserveret = 44168KB, begået = 44168KB)

Som vi kan se, er næsten 60 MB reserveret og forpligtet til at hjælpe G1.

Lad os se, hvordan hukommelsesforbruget ser ud til en meget enklere GC, siger Serial GC:

$ java -XX: NativeMemoryTracking = oversigt -Xms300m -Xmx300m -XX: + UseSerialGC -jar app.jar

Serial GC bruger næppe 1 MB:

GC (reserveret = 1034KB, begået = 1034KB) (malloc = 26KB # 158) (mmap: reserveret = 1008KB, begået = 1008KB)

Vi bør selvfølgelig ikke vælge en GC-algoritme bare på grund af dens hukommelsesforbrug, da Serial GC's stop-the-world-natur kan forårsage forringelse af ydeevnen. Der er dog flere GC'er at vælge imellem, og de balancerer hver især hukommelse og ydeevne forskelligt.

3.8. Symbol

Her er NMT-rapporten om symbolallokeringerne, såsom strengetabellen og den konstante pool:

Symbol (reserveret = 10148KB, begået = 10148KB) (malloc = 7295KB # 66194) (arena = 2853KB # 1)

Næsten 10 MB tildeles symboler.

3.9. NMT over tid

NMT giver os mulighed for at spore, hvordan hukommelsesallokeringer ændrer sig over tid. For det første skal vi markere den aktuelle tilstand for vores ansøgning som en basislinje:

$ jcmd VM.native_memory baseline Baseline lykkedes

Derefter kan vi efter et stykke tid sammenligne den aktuelle hukommelsesforbrug med den baseline:

$ jcmd VM.native_memory summary.diff

NMT, ved hjælp af + og - tegn, ville fortælle os, hvordan hukommelsesforbruget ændrede sig i denne periode:

I alt: reserveret = 1771487KB + 3373KB, forpligtet = 491491KB + 6873KB - Java Heap (reserveret = 307200KB, forpligtet = 307200KB) (mmap: reserveret = 307200KB, forpligtet = 307200KB) - Klasse (reserveret = 1084300KB + 2103KB, forpligtet = 39356KB + 2871K ) // Afkortet

Den samlede reserverede og dedikerede hukommelse steg henholdsvis med 3 MB og 6 MB. Andre udsving i hukommelsesallokeringer kan ses lige så let.

3.10. Detaljeret NMT

NMT kan give meget detaljerede oplysninger om et kort over hele hukommelsesområdet. For at aktivere denne detaljerede rapport skal vi bruge -XX: NativeMemoryTracking = detalje tuning flag.

4. Konklusion

I denne artikel opregnede vi forskellige bidragydere til indbygget hukommelsesallokering i JVM. Derefter lærte vi, hvordan man inspicerer en kørende applikation for at overvåge dens oprindelige tildelinger. Med denne indsigt kan vi mere effektivt indstille vores applikationer og størrelse vores runtime-miljøer.


$config[zx-auto] not found$config[zx-overlay] not found