Objectgroottes meten in de JVM

1. Overzicht

In deze zelfstudie gaan we zien hoeveel ruimte elk object in de Java-heap inneemt.

Eerst maken we kennis met verschillende statistieken om objectgroottes te berekenen. Vervolgens gaan we een paar manieren bekijken om instantiegroottes te meten.

Gewoonlijk maakt de geheugenlay-out van runtime-gegevensgebieden geen deel uit van de JVM-specificatie en wordt deze overgelaten aan de discretie van de implementator. Daarom kan elke JVM-implementatie een andere strategie hebben om objecten en arrays in het geheugen op te maken. Dit heeft op zijn beurt invloed op de instantiegroottes tijdens runtime.

In deze tutorial concentreren we ons op één specifieke JVM-implementatie: de HotSpot JVM.

We gebruiken de JVM- en HotSpot JVM-termen ook door elkaar in de tutorial.

2. Ondiepe, vastgehouden en diepe objectformaten

Om de objectgroottes te analyseren, kunnen we drie verschillende metrieken gebruiken: ondiep, behouden en diep.

Bij het berekenen van de ondiepe grootte van een object, kijken we alleen naar het object zelf. Dat wil zeggen, als het object verwijzingen naar andere objecten heeft, kijken we alleen naar de referentiegrootte naar de doelobjecten, niet naar hun werkelijke objectgrootte. Bijvoorbeeld:

Zoals hierboven getoond, is de ondiepe afmeting van de Verdrievoudigen instantie is slechts een som van drie referenties. We sluiten de werkelijke grootte van de verwezen objecten uit, namelijk A1, B1, en C1, vanaf deze maat.

Integendeel, de diepe afmeting van een object omvat de afmeting van alle verwezen objecten, naast de ondiepe afmeting:

Hier de diepe afmeting van de Verdrievoudigen instantie bevat drie verwijzingen plus de werkelijke grootte van A1, B1, en C1. Daarom zijn diepe maten recursief van aard.

Wanneer de GC het geheugen terugwint dat door een object wordt ingenomen, maakt het een bepaalde hoeveelheid geheugen vrij. Dat bedrag is de behouden grootte van dat object:

De behouden grootte van de Verdrievoudigen instantie omvat alleen A1 en C1 naast de Verdrievoudigen instantie zelf. Aan de andere kant omvat deze behouden grootte niet de B1, sinds de Paar instantie heeft ook een verwijzing naar B1.

Soms worden deze extra verwijzingen indirect door de JVM zelf gemaakt. Daarom kan het berekenen van de behouden grootte een gecompliceerde taak zijn.

Om de behouden omvang beter te begrijpen, moeten we denken in termen van afvalinzameling. Het verzamelen van het Verdrievoudigen instantie maakt de A1 en C1 onbereikbaar, maar de B1 is nog steeds bereikbaar via een ander object. Afhankelijk van de situatie kan de behouden maat ergens tussen de ondiepe en diepe maat liggen.

3. Afhankelijkheid

Om de geheugenlay-out van objecten of arrays in de JVM te inspecteren, gaan we de Java Object Layout (JOL) -tool gebruiken. Daarom moeten we het jol-core afhankelijkheid:

 org.openjdk.jol jol-core 0.10 

4. Eenvoudige gegevenstypen

Om de grootte van complexere objecten beter te begrijpen, moeten we eerst weten hoeveel ruimte elk eenvoudig datatype in beslag neemt. Om dat te doen, kunnen we de Java Memory Layout of JOL vragen om de VM-informatie af te drukken:

System.out.println (VM.current (). Details ());

De bovenstaande code zal de eenvoudige datatypegroottes als volgt afdrukken:

# 64-bit HotSpot VM uitvoeren. # Gebruik gecomprimeerde oop met 3-bit shift. # Gebruik gecomprimeerde klass met 3-bit shift. # Objecten zijn 8 bytes uitgelijnd. # Veldgroottes op type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Grootte array-elementen: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes ]

Dit zijn dus de ruimtevereisten voor elk eenvoudig gegevenstype in de JVM:

  • Objectreferenties verbruiken 4 bytes
  • boolean en byte waarden verbruiken 1 byte
  • kort en char waarden verbruiken 2 bytes
  • int en vlotter waarden verbruiken 4 bytes
  • lang en dubbele waarden verbruiken 8 bytes

Dit geldt voor 32-bits architecturen en ook voor 64-bits architecturen met gecomprimeerde verwijzingen.

Het is ook vermeldenswaard dat alle gegevenstypen dezelfde hoeveelheid geheugen verbruiken wanneer ze worden gebruikt als matrixcomponenttypen.

4.1. Ongecomprimeerde verwijzingen

Als we de gecomprimeerde verwijzingen uitschakelen via -XX: -UseCompressedOops afstemmingsvlag, dan veranderen de maatvereisten:

# Objecten zijn 8 bytes uitgelijnd. # Veldgroottes op type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Grootte array-elementen: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes ]

Objectreferenties verbruiken nu 8 bytes in plaats van 4 bytes. De overige gegevenstypen verbruiken nog steeds dezelfde hoeveelheid geheugen.

Bovendien kan de HotSpot JVM de gecomprimeerde verwijzingen ook niet gebruiken als de heapgrootte meer is dan 32 GB (tenzij we de uitlijning van het object wijzigen).

Het komt erop neer dat als we de gecomprimeerde verwijzingen expliciet uitschakelen of als de heapgrootte meer dan 32 GB is, de objectreferenties 8 bytes verbruiken.

Nu we het geheugengebruik voor basisgegevenstypen kennen, gaan we het berekenen voor complexere objecten.

5. Complexe objecten

Laten we, om de grootte voor complexe objecten te berekenen, eens kijken naar een typische relatie tussen professor en cursus:

openbare klas Cursus {privé Stringnaam; // constructor}

Elk Professor, kan naast de persoonlijke gegevens een lijst hebben met Cursuss:

openbare klasse Professor {privé String-naam; privé booleaanse vaste aanstelling; privélijstcursussen = nieuwe ArrayList (); privé int niveau; privé LocalDate birthDay; privé dubbele lastEvaluation; // constructor}

5.1. Ondiepe grootte: de Cursus Klasse

De ondiepe grootte van de Cursus klasse-instanties moeten een objectreferentie van 4 bytes bevatten (voor naam field) plus wat object overhead. We kunnen deze aanname controleren met JOL:

System.out.println (ClassLayout.parseClass (Course.class) .toPrintable ());

Dit zal het volgende afdrukken:

Cursusobject internals: OFFSET SIZE TYPE BESCHRIJVING WAARDE 0 12 (objectkop) Nvt 12 4 java.lang.String Course.name Nvt Instantiegrootte: 16 bytes Ruimteverlies: 0 bytes intern + 0 bytes extern = 0 bytes totaal

Zoals hierboven getoond, is de ondiepe grootte 16 bytes, inclusief een 4 bytes objectverwijzing naar de naam veld plus de objectkop.

5.2. Ondiepe grootte: de Professor Klasse

Als we dezelfde code uitvoeren voor het Professor klasse:

System.out.println (ClassLayout.parseClass (Professor.class) .toPrintable ());

Vervolgens zal JOL het geheugengebruik voor de Professor klasse zoals de volgende:

Professor object internals: OFFSET GROOTTE TYPE BESCHRIJVING WAARDE 0 12 (objectkop) n.v.t. 12 4 int Professor. Niveau n.v.t. 16 8 dubbel Professor.last Evaluatie n.v.t. 24 1 boolean Professor. Langdurig n.v.t. 25 3 (uitlijning / padding gap) 28 4 java.lang.String Professor.name nvt 32 4 java.util.List Professor.cursussen nvt 36 4 java.time.LocalDate Professor.birthDay nvt Instantiegrootte: 40 bytes Ruimteverlies: 3 bytes intern + 0 bytes extern = 3 bytes totaal

Zoals we waarschijnlijk hadden verwacht, verbruiken de ingekapselde velden 25 bytes:

  • Drie objectreferenties, die elk 4 bytes in beslag nemen. Dus 12 bytes in totaal voor het verwijzen naar andere objecten
  • een int die 4 bytes verbruikt
  • een boolean die 1 byte verbruikt
  • een dubbele die 8 bytes verbruikt

Door de 12 bytes overhead van de objectkop plus 3 bytes uitlijningsvulling toe te voegen, is de ondiepe grootte 40 bytes.

De belangrijkste afweging hier is dat we, naast de ingekapselde toestand van elk object, rekening moeten houden met de objectkop en uitlijningsvullingen bij het berekenen van verschillende objectgroottes.

5.3. Ondiepe grootte: een instantie

De De grootte van() methode in JOL biedt een veel eenvoudigere manier om de ondiepe grootte van een objectinstantie te berekenen. Als we het volgende fragment uitvoeren:

String ds = "Gegevensstructuren"; Cursus cursus = nieuwe cursus (ds); System.out.println ("De ondiepe grootte is:" + VM.current (). SizeOf (course));

Het zal de ondiepe maat als volgt afdrukken:

De ondiepe maat is: 16

5.4. Ongecomprimeerde grootte

Als we de gecomprimeerde verwijzingen uitschakelen of meer dan 32 GB van de heap gebruiken, wordt de ondiepe grootte groter:

Professor object internals: OFFSET GROOTTE TYPE BESCHRIJVING WAARDE 0 16 (objectkop) n.v.t. 16 8 dubbel Professor. Last Evaluatie n.v.t. 24 4 int Professor. Niveau n.v.t. 28 1 boolean Professor. Langdurig n.v.t. 29 3 (uitlijning / padding gap) 32 8 java.lang.String Professor.name Nvt 40 8 java.util.List Professor.cursussen Nvt 48 8 java.time.LocalDate Professor.birthDay Nvt Instantiegrootte: 56 bytes Ruimteverlies: 3 bytes intern + 0 bytes extern = 3 bytes totaal

Als de gecomprimeerde verwijzingen zijn uitgeschakeld, nemen de objectkop en de objectreferenties meer geheugen in beslag. Daarom, zoals hierboven getoond, nu hetzelfde Professor klasse verbruikt 16 extra bytes.

5.5. Diepe maat

Om de diepe afmeting te berekenen, moeten we de volledige afmeting van het object zelf en al zijn medewerkers opnemen. Bijvoorbeeld voor dit eenvoudige scenario:

String ds = "Gegevensstructuren"; Cursus cursus = nieuwe cursus (ds);

De diepe afmeting van de Cursus instantie is gelijk aan de ondiepe grootte van de Cursus instantie zelf plus de diepe omvang van dat specifieke Draad voorbeeld.

Met dat gezegd zijnde, laten we eens kijken hoeveel ruimte dat is Draad instantie verbruikt:

System.out.println (ClassLayout.parseInstance (ds) .toPrintable ());

Elk Draad instantie kapselt een char [] (hierover later meer) en een int hashcode:

java.lang.String object internals: OFFSET SIZE TYPE BESCHRIJVING WAARDE 0 4 (objectheader) 01 00 00 00 4 4 (objectheader) 00 00 00 00 8 4 (objectheader) da 02 00 f8 12 4 char [] String. waarde [D, a, t, a, S, t, r, u, c, t, u, r, e, s] 16 4 int String.hash 0 20 4 (verlies als gevolg van de volgende objectuitlijning) Instantie grootte: 24 bytes Ruimteverlies: 0 bytes intern + 4 bytes extern = 4 bytes totaal

De ondiepe omvang hiervan Draad instantie is 24 bytes, waaronder de 4 bytes gecachte hash-code, 4 bytes van char [] referentie en andere typische objectoverhead.

Om de werkelijke grootte van de char [], we kunnen ook zijn klasse-indeling ontleden:

System.out.println (ClassLayout.parseInstance (ds.toCharArray ()). ToPrintable ());

De indeling van het char [] het lijkt op dit:

[Interne objecten C: OFFSET GROOTTE TYPE BESCHRIJVING WAARDE 0 4 (objectkop) 01 00 00 00 4 4 (objectkop) 00 00 00 00 8 4 (objectkop) 41 00 00 f8 12 4 (objectkop) 0f 00 00 00 16 30 karakters [C. Nvt 46 2 (verlies als gevolg van de volgende objectuitlijning) Instantiegrootte: 48 bytes Ruimteverlies: 0 bytes intern + 2 bytes extern = 2 bytes totaal

We hebben dus 16 bytes voor de Cursus bijvoorbeeld 24 bytes voor de Draad instantie, en tot slot 48 bytes voor de char []. In totaal de diepe omvang daarvan Cursus instantie is 88 bytes.

Met de introductie van compacte strings in Java 9, de Draad klasse gebruikt intern een byte[] om de karakters op te slaan:

java.lang.String object internals: OFFSET SIZE TYPE BESCHRIJVING 0 4 (objectheader) 4 4 (objectheader) 8 4 (objectheader) 12 4 byte [] String.value # de byte-array 16 4 int String.hash 20 1 byte String.coder # encodig 21 3 (verlies door de volgende objectuitlijning)

Daarom is op Java 9+ de totale footprint van het Cursus instantie zal 72 bytes zijn in plaats van 88 bytes.

5.6. Object grafieklay-out

In plaats van de klasselay-out van elk object in een objectgrafiek afzonderlijk te ontleden, kunnen we de GraphLayout. Met GraphLayot, we passeren gewoon het startpunt van de objectgrafiek en het rapporteert de lay-out van alle bereikbare objecten vanaf dat startpunt. Op deze manier kunnen we de diepte van het startpunt van de grafiek berekenen.

We kunnen bijvoorbeeld de totale voetafdruk van de Cursus bijvoorbeeld als volgt:

System.out.println (GraphLayout.parseInstance (cursus) .toFootprint ());

Welke de volgende samenvatting afdrukt:

[email protected] footprint: COUNT AVG SUM BESCHRIJVING 1 48 48 [C 1 16 16 com.baeldung.objectsize.Course 1 24 24 java.lang.String 3 88 (totaal)

Dat zijn in totaal 88 bytes. De totale grootte() methode retourneert de totale footprint van het object, dat is 88 bytes:

System.out.println (GraphLayout.parseInstance (cursus) .totalSize ());

6. Instrumentatie

Om de ondiepe grootte van een object te berekenen, kunnen we ook het Java-instrumentatiepakket en Java-agents gebruiken. Eerst moeten we een klasse maken met een premain () methode:

openbare klasse ObjectSizeCalculator {privé statische instrumentatie-instrumentatie; openbare statische leegte premain (String args, Instrumentation inst) {instrumentation = inst; } openbare statische long sizeOf (Object o) {return instrumentation.getObjectSize (o); }}

Zoals hierboven weergegeven, gebruiken we de getObjectSize () methode om de ondiepe afmeting van een object te vinden. We hebben ook een manifestbestand nodig:

Premain-klasse: com.baeldung.objectsize.ObjectSizeCalculator

Gebruik dit dan MANIFEST.MF bestand, kunnen we een JAR-bestand maken en het als een Java-agent gebruiken:

$ jar cmf MANIFEST.MF agent.jar * .class

Ten slotte, als we een code uitvoeren met de -javaagent: /path/to/agent.jar argument, dan kunnen we de De grootte van() methode:

String ds = "Gegevensstructuren"; Cursuscursus = nieuwe cursus (ds); System.out.println (ObjectSizeCalculator.sizeOf (cursus));

Hiermee wordt 16 afgedrukt als de ondiepe afmeting van de Cursus voorbeeld.

7. Klasstatistieken

Om de ondiepe grootte van objecten in een reeds draaiende applicatie te zien, kunnen we de klassenstatistieken bekijken met behulp van de jcmd:

$ jcmd GC.class_stats [output_columns]

We kunnen bijvoorbeeld elke instantiegrootte en het aantal Cursus gevallen:

$ jcmd 63984 GC.class_stats InstSize, InstCount, InstBytes | grep Course 63984: InstSize InstCount InstBytes ClassName 16 1 16 com.baeldung.objectsize.Course

Nogmaals, dit rapporteert de ondiepe grootte van elk Cursus instantie als 16 bytes.

Om de klassenstatistieken te zien, moeten we de applicatie starten met de -XX: + UnlockDiagnosticVMOptions tuning vlag.

8. Heap Dump

Het gebruik van heap-dumps is een andere optie om de instantiegroottes in actieve applicaties te inspecteren. Op deze manier kunnen we de behouden grootte voor elke instantie zien. Om een ​​heap-dump te maken, kunnen we de jcmd als het volgende:

$ jcmd GC.heap_dump [opties] / pad / naar / dump / bestand

Bijvoorbeeld:

$ jcmd 63984 GC.heap_dump -all ~ / dump.hpro

Hierdoor wordt een heap-dump op de opgegeven locatie gemaakt. Ook met de -alle optie, zullen alle bereikbare en onbereikbare objecten aanwezig zijn in de heap dump. Zonder deze optie voert de JVM een volledige GC uit voordat de heap-dump wordt gemaakt.

Nadat we de heap-dump hebben opgehaald, kunnen we deze importeren in tools zoals Visual VM:

Zoals hierboven getoond, is de behouden grootte van de enige Cursus instantie is 24 bytes. Zoals eerder vermeld, kan de vastgehouden grootte ergens tussen ondiep (16 bytes) en diep (88 bytes) liggen.

Het is ook vermeldenswaard dat de Visual VM deel uitmaakte van de Oracle- en Open JDK-distributies vóór Java 9. Dit is echter niet langer het geval vanaf Java 9 en we zouden de Visual VM afzonderlijk van zijn website moeten downloaden.

9. Conclusie

In deze zelfstudie hebben we kennis gemaakt met verschillende statistieken om objectgroottes in de JVM-runtime te meten. Daarna hebben we de instantiegroottes daadwerkelijk gemeten met verschillende tools zoals JOL, Java Agents en het jcmd opdrachtregelprogramma.

Zoals gewoonlijk zijn alle voorbeelden beschikbaar op GitHub.