Native Memory Tracking in JVM

1. Overzicht

Ooit afgevraagd waarom Java-applicaties veel meer geheugen verbruiken dan de opgegeven hoeveelheid via de bekende -Xms en -Xmx vlaggen afstemmen? Om verschillende redenen en mogelijke optimalisaties kan de JVM extra native geheugen toewijzen. Deze extra toewijzingen kunnen uiteindelijk het verbruikte geheugen boven de -Xmx beperking.

In deze zelfstudie gaan we een paar veelvoorkomende bronnen van native geheugentoewijzingen in de JVM opsommen, samen met hun afstemmingsvlaggen voor de grootte, en vervolgens leren we hoe u Native Memory Tracking om ze te volgen.

2. Native toewijzingen

De heap is meestal de grootste verbruiker van geheugen in Java-toepassingen, maar er zijn andere. Naast de heap wijst de JVM een vrij groot deel van het oorspronkelijke geheugen toe om de metagegevens van de klasse, applicatiecode, de code gegenereerd door JIT, interne datastructuren, enz. Te behouden. In de volgende secties zullen we enkele van die toewijzingen onderzoeken.

2.1. Metaspace

Om enkele metagegevens over de geladen klassen te behouden, gebruikt The JVM een speciaal niet-heapgebied genaamd Metaspace. Vóór Java 8 heette het equivalent PermGen of Permanente generatie. Metaspace of PermGen bevat de metadata over de geladen klassen in plaats van de instanties ervan, die binnen de heap worden bewaard.

Het belangrijkste hier is dat de heap-formaatconfiguraties hebben geen invloed op de metaspace-grootte aangezien de Metaspace een off-heap gegevensgebied is. Om de Metaspace-grootte te beperken, gebruiken we andere afstemmingsvlaggen:

  • -XX: MetaspaceSize en -XX: MaxMetaspaceSize om de minimale en maximale metaspace-grootte in te stellen
  • Voordat Java 8, -XX: PermSize en -XX: MaxPermSize om de minimale en maximale PermGen-grootte in te stellen

2.2. Draden

Een van de meest geheugenintensieve gegevensgebieden in de JVM is de stapel, die tegelijkertijd met elke thread wordt gemaakt. De stapel slaat lokale variabelen en deelresultaten op en speelt een belangrijke rol bij het aanroepen van methoden.

De standaardgrootte van de threadstapel is platformafhankelijk, maar in de meeste moderne 64-bits besturingssystemen is deze ongeveer 1 MB. Deze maat is configureerbaar via de -Xss tuning vlag.

In tegenstelling tot andere gegevensgebieden, het totale geheugen dat aan stapels wordt toegewezen, is praktisch onbegrensd als er geen beperking is op het aantal threads. Het is ook vermeldenswaard dat de JVM zelf een paar threads nodig heeft om zijn interne bewerkingen uit te voeren, zoals GC of just-in-time-compilaties.

2.3. Code Cache

Om JVM-bytecode op verschillende platforms uit te voeren, moet deze worden geconverteerd naar machine-instructies. De JIT-compiler is verantwoordelijk voor deze compilatie terwijl het programma wordt uitgevoerd.

Wanneer de JVM bytecode compileert naar montage-instructies, slaat het die instructies op in een speciaal niet-heap-gegevensgebied genaamd Code Cache. De codecache kan net als andere gegevensgebieden in de JVM worden beheerd. De -XX: InitialCodeCacheSize en -XX: ReservedCodeCacheSize afstemmingsvlaggen bepalen de initiële en maximaal mogelijke grootte voor de codecache.

2.4. Garbage Collection

De JVM wordt geleverd met een handvol GC-algoritmen, elk geschikt voor verschillende gebruikssituaties. Al die GC-algoritmen delen één gemeenschappelijk kenmerk: ze hebben een aantal off-heap datastructuren nodig om hun taken uit te voeren. Deze interne datastructuren verbruiken meer native geheugen.

2.5. Symbolen

Laten we beginnen met Snaren, een van de meest gebruikte gegevenstypen in applicatie- en bibliotheekcode. Vanwege hun alomtegenwoordigheid bezetten ze meestal een groot deel van de hoop. Als een groot aantal van die strings dezelfde inhoud bevat, gaat een aanzienlijk deel van de hoop verloren.

Om wat heapruimte te besparen, kunnen we van elk een versie opslaan Draad en anderen te laten verwijzen naar de opgeslagen versie. Dit proces heet String Interning.Omdat de JVM alleen stage kan lopen Compile Time String Constants, we kunnen handmatig de intern() methode op strings die we willen stagiair.

JVM slaat geïnterneerde strings op in een speciale native hashtabel met een vaste grootte, de String tafel, ook wel bekend als de String Zwembad. We kunnen de tafelgrootte (d.w.z. het aantal emmers) configureren via de -XX: StringTableSize tuning vlag.

Naast de stringtabel is er nog een ander native datagebied, de Runtime constante pool. JVM gebruikt deze pool om constanten op te slaan, zoals numerieke literalen tijdens het compileren of methode- en veldreferenties die tijdens runtime moeten worden opgelost.

2.6. Native Byte Buffers

De JVM is de gebruikelijke verdachte voor een aanzienlijk aantal native allocaties, maar soms kunnen ontwikkelaars ook direct native geheugen toewijzen. De meest voorkomende benaderingen zijn de malloc oproep door JNI en NIO's direct ByteBuffers.

2.7. Extra afstemmingsvlaggen

In deze sectie hebben we een handvol JVM-afstemmingsvlaggen gebruikt voor verschillende optimalisatiescenario's. Met behulp van de volgende tip kunnen we bijna alle afstemmingsvlaggen vinden die betrekking hebben op een bepaald concept:

$ java -XX: + PrintFlagsFinal -versie | grep 

De PrintFlagsFinal drukt alle -XX opties in JVM. Om bijvoorbeeld alle Metaspace-gerelateerde vlaggen te vinden:

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

3. Native Memory Tracking (NMT)

Nu we de gemeenschappelijke bronnen van native geheugentoewijzingen in de JVM kennen, is het tijd om uit te zoeken hoe we ze kunnen controleren. Ten eerste moeten we het volgen van het native geheugen inschakelen met nog een andere JVM-afstemmingsvlag: -XX: NativeMemoryTracking = uit | samenvatting | detail. Standaard is de NMT uitgeschakeld, maar we kunnen hem inschakelen om een ​​samenvatting of gedetailleerde weergave van zijn waarnemingen te zien.

Stel dat we native allocaties willen volgen voor een typische Spring Boot-applicatie:

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

Hier schakelen we de NMT in terwijl we 300 MB heapruimte toewijzen, met G1 als ons GC-algoritme.

3.1. Directe momentopnamen

Als NMT is ingeschakeld, kunnen we de native geheugeninformatie op elk moment ophalen met behulp van de jcmd opdracht:

$ jcmd VM.native_memory

Om de PID voor een JVM-toepassing te vinden, kunnen we de jpsopdracht:

$ jps -l 7858 app.jar // Dit is onze app 7899 sun.tools.jps.Jps

Als we nu gebruiken jcmd met de juiste pid, de VM.native_memory zorgt ervoor dat de JVM de informatie over native allocaties afdrukt:

$ jcmd 7858 VM.native_memory

Laten we de NMT-output sectie voor sectie analyseren.

3.2. Totale toewijzingen

NMT rapporteert het totale gereserveerde en toegewezen geheugen als volgt:

Native Memory Tracking: Totaal: gereserveerd = 1731124 KB, vastgelegd = 448152 KB

Gereserveerd geheugen vertegenwoordigt de totale hoeveelheid geheugen die onze app mogelijk kan gebruiken. Omgekeerd is het toegewezen geheugen gelijk aan de hoeveelheid geheugen die onze app momenteel gebruikt.

Ondanks het toewijzen van 300 MB aan heap, is het totale gereserveerde geheugen voor onze app bijna 1,7 GB, veel meer dan dat. Evenzo is het toegewezen geheugen ongeveer 440 MB, wat opnieuw veel meer is dan die 300 MB.

Na de totale sectie rapporteert NMT geheugentoewijzingen per allocatiebron. Laten we dus elke bron grondig onderzoeken.

3.3. Hoop

NMT rapporteert onze heap-toewijzingen zoals we hadden verwacht:

Java-heap (gereserveerd = 307200KB, doorgevoerd = 307200KB) (mmap: gereserveerd = 307200KB, doorgevoerd = 307200KB)

300 MB gereserveerd en toegewezen geheugen, wat overeenkomt met onze heapgrootte-instellingen.

3.4. Metaspace

Dit is wat de NMT zegt over de klasse-metadata voor geladen klassen:

Klasse (gereserveerd = 1091407KB, gecommitteerd = 45815KB) (klassen # 6566) (malloc = 10063KB # 8519) (mmap: gereserveerd = 1081344KB, gecommitteerd = 35752KB)

Bijna 1 GB gereserveerd en 45 MB gereserveerd voor het laden van 6566 klassen.

3.5. Draad

En hier is het NMT-rapport over threadtoewijzingen:

Thread (gereserveerd = 37018KB, vastgelegd = 37018KB) (thread # 37) (stack: gereserveerd = 36864KB, toegewijd = 36864KB) (malloc = 112KB # 190) (arena = 42KB # 72)

In totaal is 36 MB geheugen toegewezen aan stapels voor 37 threads - bijna 1 MB per stapel. JVM wijst het geheugen toe aan threads op het moment van maken, zodat de gereserveerde en vastgelegde toewijzingen gelijk zijn.

3.6. Code Cache

Laten we eens kijken wat NMT zegt over de gegenereerde en gecachte montage-instructies door JIT:

Code (gereserveerd = 251549KB, vastgelegd = 14169KB) (malloc = 1949KB # 3424) (mmap: gereserveerd = 249600KB, vastgelegd = 12220KB)

Momenteel wordt bijna 13 MB code in de cache opgeslagen, en dit aantal kan oplopen tot ongeveer 245 MB.

3.7. GC

Hier is het NMT-rapport over het geheugengebruik van de G1 GC:

GC (gereserveerd = 61771KB, vastgelegd = 61771KB) (malloc = 17603KB # 4501) (mmap: gereserveerd = 44168KB, vastgelegd = 44168KB)

Zoals we kunnen zien, is bijna 60 MB gereserveerd en toegewijd om G1 te helpen.

Laten we eens kijken hoe het geheugengebruik eruitziet voor een veel eenvoudigere GC, zeg maar Serial GC:

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

De seriële GC gebruikt amper 1 MB:

GC (gereserveerd = 1034 KB, vastgelegd = 1034 KB) (malloc = 26 KB # 158) (mmap: gereserveerd = 1008 KB, vastgelegd = 1008 KB)

Het is duidelijk dat we geen GC-algoritme moeten kiezen alleen vanwege het geheugengebruik, omdat het stop-de-wereld-karakter van de seriële GC prestatieverminderingen kan veroorzaken. Er zijn echter verschillende GC's om uit te kiezen, en ze balanceren elk geheugen en prestaties op een andere manier.

3.8. Symbool

Hier is het NMT-rapport over de symbooltoewijzingen, zoals de stringtabel en constante pool:

Symbool (gereserveerd = 10148KB, vastgelegd = 10148KB) (malloc = 7295KB # 66194) (arena = 2853KB # 1)

Bijna 10 MB is toegewezen aan symbolen.

3.9. NMT in de loop van de tijd

Met de NMT kunnen we bijhouden hoe geheugentoewijzingen in de loop van de tijd veranderen. Ten eerste moeten we de huidige status van onze applicatie markeren als een basislijn:

$ jcmd VM.native_memory baseline Baseline geslaagd

Daarna kunnen we na een tijdje het huidige geheugengebruik vergelijken met die basislijn:

$ jcmd VM.native_memory summary.diff

NMT, met behulp van + en - tekens, zou ons vertellen hoe het geheugengebruik in die periode veranderde:

Totaal: gereserveerd = 1771487 KB + 3373 KB, gecommitteerd = 491491 KB + 6873 KB - Java Heap (gereserveerd = 307200 KB, gecommitteerd = 307200 KB) (mmap: gereserveerd = 307200 KB, gecommitteerd = 307200 KB) - Klasse (gereserveerd = 1084300 KB + 2103 KB, gecommitteerd = 39356 KB + 2871 KB ) // Afgekapt

Het totale gereserveerde en toegewezen geheugen nam toe met respectievelijk 3 MB en 6 MB. Andere fluctuaties in geheugentoewijzingen kunnen net zo gemakkelijk worden opgemerkt.

3.10. Gedetailleerde NMT

NMT kan zeer gedetailleerde informatie geven over een kaart van de gehele geheugenruimte. Om dit gedetailleerde rapport mogelijk te maken, moeten we de -XX: NativeMemoryTracking = detail tuning vlag.

4. Conclusie

In dit artikel hebben we verschillende bijdragers aan native geheugentoewijzingen in de JVM opgesomd. Vervolgens hebben we geleerd hoe we een actieve applicatie kunnen inspecteren om de native toewijzingen te bewaken. Met deze inzichten kunnen we onze applicaties effectiever afstemmen en onze runtime-omgevingen vergroten.