Native Memory Tracking i JVM

1. Oversikt

Noen gang lurt på hvorfor Java-applikasjoner bruker mye mer minne enn den angitte mengden via det velkjente -Xms og -Xmx tuning flagg? Av en rekke årsaker og mulige optimaliseringer kan JVM tildele ekstra eget minne. Disse ekstra tildelingene kan til slutt øke forbruket minne utover -Xmx begrensning.

I denne opplæringen skal vi oppregne noen få vanlige kilder for tilordnede minnetildelinger i JVM, sammen med størrelsesjusteringsflaggene, og deretter lære å bruke Native Memory Tracking for å overvåke dem.

2. Innfødte tildelinger

Bunken er vanligvis den største forbrukeren av minne i Java-applikasjoner, men det er andre. Foruten bunken, tildeler JVM en ganske stor del fra det opprinnelige minnet for å opprettholde klassemetadataene, applikasjonskoden, koden generert av JIT, interne datastrukturer, etc. I de følgende avsnittene vil vi utforske noen av disse tildelingene.

2.1. Metaspace

For å opprettholde noen metadata om de lastede klassene, bruker JVM et dedikert område som ikke er bunke Metaspace. Før Java 8 ble ekvivalenten kalt PermGen eller Permanent generasjon. Metaspace eller PermGen inneholder metadataene om de lastede klassene i stedet for forekomster av dem, som holdes inne i dyngen.

Det viktige her er at konfigurasjonene av haugstørrelse vil ikke påvirke Metaspace-størrelsen siden Metaspace er et off-heap dataområde. For å begrense Metaspace-størrelsen bruker vi andre tuningflagg:

  • -XX: MetaspaceSize og -XX: MaxMetaspaceSize for å angi minimum og maksimal Metaspace-størrelse
  • Før Java 8, -XX: PermSize og -XX: MaxPermSize for å angi minimum og maksimum PermGen størrelse

2.2. Tråder

Et av de mest minnekrevende dataområdene i JVM er stabelen, opprettet samtidig som hver tråd. Stabelen lagrer lokale variabler og delvise resultater, og spiller en viktig rolle i metodeanropninger.

Standard trådstabelstørrelse er plattformavhengig, men i de fleste moderne 64-biters operativsystemer er den rundt 1 MB. Denne størrelsen kan konfigureres via -Xss innstillingsflagg.

I motsetning til andre dataområder, det totale minnet som er tildelt stabler er praktisk talt ubegrenset når det ikke er noen begrensning på antall tråder. Det er også verdt å nevne at JVM selv trenger noen tråder for å utføre sine interne operasjoner som GC eller just-in-time kompilasjoner.

2.3. Kodebuffer

For å kjøre JVM bytecode på forskjellige plattformer, må den konverteres til maskininstruksjoner. JIT-kompilatoren er ansvarlig for denne samlingen når programmet kjøres.

Når JVM kompilerer bytekode til monteringsinstruksjoner, lagrer den instruksjonene i et spesielt ikke-bunke dataområde kalt Kodebuffer. Kodebufferen kan administreres akkurat som andre dataområder i JVM. De -XX: InitialCodeCacheSize og -XX: ReservedCodeCacheSize innstillingsflagg bestemmer den innledende og maksimale mulige størrelsen for kodebufferen.

2.4. Søppelsamling

JVM-en leveres med en håndfull GC-algoritmer, som hver er egnet for forskjellige bruksområder. Alle disse GC-algoritmene deler et felles trekk: de trenger å bruke noen off-heap datastrukturer for å utføre sine oppgaver. Disse interne datastrukturene bruker mer naturlig minne.

2.5. Symboler

La oss starte med Strenger, en av de mest brukte datatypene i applikasjon og bibliotekode. På grunn av sin allestedsnærværende okkuperer de vanligvis en stor del av haugen. Hvis et stort antall av strengene inneholder det samme innholdet, vil en betydelig del av dyngen være bortkastet.

For å spare litt masse plass, kan vi lagre en versjon av hver String og få andre til å henvise til den lagrede versjonen. Denne prosessen kalles String Interning.Siden JVM bare kan praktisere Kompilere tidsstrengkonstanter, vi kan kalle manuelt turnuskandidat() metode på strenger vi har tenkt å praktisere.

JVM lagrer internerte strenger i en spesiell innfødt fast størrelse hashtable kalt String Table, også kjent som String Pool. Vi kan konfigurere tabellstørrelsen (dvs. antall skuffer) via -XX: StringTableSize innstillingsflagg.

I tillegg til strengtabellen er det et annet innfødt dataområde som heter Runtime Constant Pool. JVM bruker dette bassenget til å lagre konstanter som numeriske bokstaver for kompilering eller metode- og feltreferanser som må løses ved kjøretid.

2.6. Innfødte bytebuffere

JVM er den vanlige mistenkte for et betydelig antall innfødte tildelinger, men noen ganger kan utviklere også tildele direkte innfødt minne. De vanligste tilnærmingene er malloc ring av JNI og NIOs direkte ByteBuffers.

2.7. Ekstra tuningflagg

I denne delen brukte vi en håndfull JVM-tuningflagg for forskjellige optimaliseringsscenarier. Ved å bruke følgende tips kan vi finne nesten alle innstillingsflagger relatert til et bestemt konsept:

$ java -XX: + PrintFlagsFinal -versjon | grep 

De PrintFlagsFinal skriver ut alle -XX alternativer i JVM. For eksempel for å finne alle Metaspace-relaterte flagg:

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

3. Native Memory Tracking (NMT)

Nå som vi kjenner til de vanlige kildene til tilordnede minnetildelinger i JVM, er det på tide å finne ut hvordan du kan overvåke dem. Først bør vi aktivere det opprinnelige minnesporingen ved å bruke enda et JVM-tuningflagg: -XX: NativeMemoryTracking = av | sumary | detalj. Som standard er NMT av, men vi kan gjøre det mulig å se et sammendrag eller detaljert syn på observasjonene.

La oss anta at vi vil spore innfødte tildelinger for en typisk Spring Boot-applikasjon:

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

Her aktiverer vi NMT mens vi tildeler 300 MB dyngdeplass, med G1 som GC-algoritme.

3.1. Øyeblikkelige øyeblikksbilder

Når NMT er aktivert, kan vi når som helst få informasjon om det opprinnelige minnet ved hjelp av jcmd kommando:

$ jcmd VM.native_memory

For å finne PID for en JVM-applikasjon, kan vi bruke jpskommando:

$ jps -l 7858 app.jar // Dette er appen vår 7899 sun.tools.jps.Jps

Nå hvis vi bruker jcmd med det aktuelle pid, den VM.native_memory får JVM til å skrive ut informasjonen om innfødte tildelinger:

$ jcmd 7858 VM.native_memory

La oss analysere NMT-utgang seksjon for seksjon.

3.2. Totale tildelinger

NMT rapporterer det totale reserverte og engasjerte minnet som følger:

Native Memory Tracking: Total: reservert = 1731124KB, begått = 448152KB

Reservert minne representerer den totale mengden minne appen vår potensielt kan bruke. Omvendt er det dedikerte minnet lik mengden minne appen vår bruker akkurat nå.

Til tross for tildeling av 300 MB haug, er det totale reserverte minnet for appen vår nesten 1,7 GB, mye mer enn det. Tilsvarende er det dedikerte minnet rundt 440 MB, som igjen er mye mer enn det 300 MB.

Etter den totale delen rapporterer NMT minnetildelinger per tildelingskilde. Så la oss utforske hver kilde i dybden.

3.3. Haug

NMT rapporterer om haugetildelingene som vi forventet:

Java Heap (reservert = 307200KB, forpliktet = 307200KB) (mmap: reservert = 307200KB, forpliktet = 307200KB)

300 MB med både reservert og dedikert minne, som samsvarer med innstillingene for haugestørrelse.

3.4. Metaspace

Her er hva NMT sier om klassens metadata for lastede klasser:

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

Nesten 1 GB reservert og 45 MB forpliktet til å laste 6566 klasser.

3.5. Tråd

Og her er NMT-rapporten om trådallokeringer:

Tråd (reservert = 37018KB, begått = 37018KB) (tråd nr. 37) (stabel: reservert = 36864KB, forpliktet = 36864KB) (malloc = 112KB # 190) (arena = 42KB # 72)

Totalt tildeles 36 MB minne til stabler for 37 tråder - nesten 1 MB per bunke. JVM tildeler minnet til tråder på tidspunktet for opprettelsen, så de reserverte og engasjerte tildelingene er like.

3.6. Kodebuffer

La oss se hva NMT sier om de genererte og hurtigbufrede monteringsinstruksjonene fra JIT:

Kode (reservert = 251549KB, forpliktet = 14169KB) (malloc = 1949KB # 3424) (mmap: reservert = 249600KB, forpliktet = 12220KB)

For øyeblikket lagres nesten 13 MB kode, og dette beløpet kan potensielt gå opp til omtrent 245 MB.

3.7. GC

Her er NMT-rapporten om G1 GCs minnebruk:

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

Som vi kan se, er nesten 60 MB reservert og forpliktet til å hjelpe G1.

La oss se hvordan minnebruk ser ut for en mye enklere GC, si Serial GC:

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

Serial GC bruker knapt 1 MB:

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

Vi burde åpenbart ikke velge en GC-algoritme bare på grunn av minnebruk, da Serial GC's stopp-verden-natur kan forårsake ytelsesforringelser. Det er imidlertid flere GC-er å velge mellom, og de balanserer forskjellig minne og ytelse.

3.8. Symbol

Her er NMT-rapporten om symboltildelingen, for eksempel strengetabellen og konstant basseng:

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

Nesten 10 MB er tildelt symboler.

3.9. NMT over tid

NMT lar oss spore hvordan minnetildelingene endres over tid. Først bør vi merke den nåværende tilstanden til søknaden vår som en grunnlinje:

$ jcmd VM.native_memory baseline Baseline lyktes

Så, etter en stund, kan vi sammenligne gjeldende minnebruk med den grunnlinjen:

$ jcmd VM.native_memory summary.diff

NMT, ved hjelp av + og - tegn, ville fortelle oss hvordan minnebruk endret seg over den perioden:

Totalt: reservert = 1771487KB + 3373KB, forpliktet = 491491KB + 6873KB - Java Heap (reservert = 307200KB, forpliktet = 307200KB) (mmap: reservert = 307200KB, forpliktet = 307200KB) - Klasse (reservert = 1084300KB + 2103KB, forpliktet = 39356KB + 2871K ) // Avkortet

Det totale reserverte og engasjerte minnet økte med henholdsvis 3 MB og 6 MB. Andre svingninger i minnetildelingene kan sees like enkelt.

3.10. Detaljert NMT

NMT kan gi veldig detaljert informasjon om et kart over hele minneplassen. For å aktivere denne detaljerte rapporten, bør vi bruke -XX: NativeMemoryTracking = detalj innstillingsflagg.

4. Konklusjon

I denne artikkelen oppsummerte vi forskjellige bidragsytere til tilordnede minnetildelinger i JVM. Deretter lærte vi hvordan vi inspiserte et program som kjører for å overvåke dets opprinnelige tildelinger. Med denne innsikten kan vi mer effektivt stille inn applikasjonene våre og størrelsen på kjøretidsmiljøene.