Java >> Java tutorial >  >> JVM

Måling af objektstørrelser i JVM

1. Oversigt

I denne øvelse skal vi se, hvor meget plads hvert objekt bruger i Java-bunken.

Først vil vi blive fortrolige med forskellige metrikker til at beregne objektstørrelser. Derefter skal vi se et par måder at måle instansstørrelser på.

Normalt er hukommelseslayoutet af runtime-dataområder ikke en del af JVM-specifikationen og overlades til implementatorens skøn. Derfor kan hver JVM-implementering have en anden strategi til at layoute objekter og arrays i hukommelsen. Dette vil til gengæld påvirke instansstørrelserne under kørsel.

I denne øvelse fokuserer vi på én specifik JVM-implementering:HotSpot JVM.

Vi bruger også JVM- og HotSpot JVM-udtrykkene i flæng gennem hele selvstudiet.

2. Lavvandede, fastholdte og dybe objektstørrelser

Til at analysere objektstørrelserne kan vi bruge tre forskellige metrikker:Lavvandede, fastholdte og dybe størrelser.

Når vi beregner den lave størrelse af et objekt, tager vi kun hensyn til selve objektet. Det vil sige, at hvis objektet har referencer til andre objekter, tager vi kun hensyn til referencestørrelsen til målobjekterne, ikke deres faktiske objektstørrelse. For eksempel:

Som vist ovenfor er den lave størrelse af Triple  instans er kun en sum af tre referencer. Vi ekskluderer den faktiske størrelse af de henviste objekter, nemlig A1, B1,  og C1,  fra denne størrelse.

Tværtimod, den dybe størrelse af et objekt inkluderer størrelsen af ​​alle henviste objekter, ud over den lave størrelse:

Her er den dybe størrelse af Triple  instans indeholder tre referencer plus den faktiske størrelse af A1, B1,  og C1. Derfor er dybe størrelser af rekursive karakter.

Når GC'en genvinder hukommelsen optaget af et objekt, frigiver den en bestemt mængde hukommelse. Dette beløb er den bevarede størrelse af det pågældende objekt:

Den bevarede størrelse af Trippel  forekomst inkluderer kun A1  og C1 ud over Trippel  selve instansen. På den anden side inkluderer denne bevarede størrelse ikke B1,  siden parret  instans har også en reference til B1.

Nogle gange er disse ekstra referencer indirekte lavet af JVM selv. Derfor kan det være en kompliceret opgave at beregne den bevarede størrelse.

For bedre at forstå den bevarede størrelse, bør vi tænke i affaldsindsamlingen. Indsamling af Trippel  instans gør A1  og C1  utilgængelig, men B1  er stadig tilgængelig via et andet objekt. Afhængigt af situationen kan den bevarede størrelse være hvor som helst mellem den lave og dybe størrelse.

3. Afhængighed

For at inspicere hukommelseslayoutet af objekter eller arrays i JVM'en skal vi bruge værktøjet Java Object Layout (JOL). Derfor bliver vi nødt til at tilføje jol-kernen afhængighed:

<dependency> 
    <groupId>org.openjdk.jol</groupId> 
    <artifactId>jol-core</artifactId>    
    <version>0.10</version> 
</dependency>

4. Simple datatyper

For at få en bedre forståelse af størrelsen af ​​mere komplekse objekter bør vi først vide, hvor meget plads hver enkelt datatype bruger. For at gøre det kan vi bede Java Memory Layout eller JOL om at udskrive VM-oplysningerne:

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

Ovenstående kode vil udskrive de simple datatypestørrelser som følger:

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Så her er pladskravene for hver enkelt datatype i JVM:

  • Objektreferencer bruger 4 bytes
  • boolesk  og byte  værdier bruger 1 byte
  • kort  og char  værdier bruger 2 bytes
  • int  og flydende  værdier bruger 4 bytes
  • lang  og dobbelt  værdier bruger 8 bytes

Dette er sandt i 32-bit arkitekturer og også 64-bit arkitekturer med komprimerede referencer i kraft.

Det er også værd at nævne, at alle datatyper bruger den samme mængde hukommelse, når de bruges som array-komponenttyper.

4.1. Ukomprimerede referencer

Hvis vi deaktiverer de komprimerede referencer via -XX:-UseCompressedOops  tuning flag, så ændres størrelseskravene:

# Objects are 8 bytes aligned.
# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Nu vil objektreferencer forbruge 8 bytes i stedet for 4 bytes. De resterende datatyper bruger stadig den samme mængde hukommelse.

Desuden kan HotSpot JVM heller ikke bruge de komprimerede referencer, når heapstørrelsen er mere end 32 GB (medmindre vi ændrer objektjusteringen).

Bundlinjen er, at hvis vi eksplicit deaktiverer de komprimerede referencer, eller heapstørrelsen er mere end 32 GB, vil objektreferencerne forbruge 8 bytes.

Nu hvor vi kender hukommelsesforbruget for grundlæggende datatyper, lad os beregne det for mere komplekse objekter.

5. Komplekse objekter

For at beregne størrelsen for komplekse objekter, lad os overveje et typisk forhold mellem professor og kursus:

public class Course {

    private String name;

    // constructor
}

Hver professor  ud over de personlige oplysninger, kan have en liste over Kursus s:

public class Professor {

    private String name;
    private boolean tenured;
    private List<Course> courses = new ArrayList<>();
    private int level;
    private LocalDate birthDay;
    private double lastEvaluation;

    // constructor
}

5.1. Lav størrelse:Kurset Klasse

Den overfladiske størrelse af Kurset  klasseforekomster skal indeholde en 4-byte objektreference (for navn  felt) plus noget objekt overhead. Vi kan kontrollere denne antagelse ved hjælp af JOL:

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

Dette vil udskrive følgende:

Course object internals:
 OFFSET  SIZE               TYPE DESCRIPTION               VALUE
      0    12                    (object header)           N/A
     12     4   java.lang.String Course.name               N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

Som vist ovenfor er den lave størrelse 16 bytes, inklusive en 4 bytes objektreference til navnet  felt plus objekthovedet.

5.2. Lav størrelse:Professoren  Klasse

Hvis vi kører den samme kode for Professor  klasse:

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

Derefter udskriver JOL hukommelsesforbruget for professoren  klasse som følgende:

Professor object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION                     VALUE
      0    12                       (object header)                 N/A
     12     4                   int Professor.level                 N/A
     16     8                double Professor.lastEvaluation        N/A
     24     1               boolean Professor.tenured               N/A
     25     3                       (alignment/padding gap)                  
     28     4      java.lang.String Professor.name                  N/A
     32     4        java.util.List Professor.courses               N/A
     36     4   java.time.LocalDate Professor.birthDay              N/A
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

Som vi nok havde forventet, bruger de indkapslede felter 25 bytes:

  • Tre objektreferencer, som hver bruger 4 bytes. Altså 12 bytes i alt for at henvise til andre objekter
  • Én int  som bruger 4 bytes
  • En boolesk  som bruger 1 byte
  • En dobbelt  som bruger 8 bytes

Tilføjes de 12 bytes overhead af objekthovedet plus 3 bytes justeringsudfyldning, er den lave størrelse 40 bytes.

Det vigtigste her er, ud over den indkapslede tilstand for hvert objekt, at vi bør overveje objektets overskrift og justering af polstringer, når vi beregner forskellige objektstørrelser.

5.3. Lav størrelse:en instans

sizeOf()  metode i JOL giver en meget enklere måde at beregne den lave størrelse af en objektforekomst på. Hvis vi kører følgende uddrag:

String ds = "Data Structures";
Course course = new Course(ds);

System.out.println("The shallow size is: " + VM.current().sizeOf(course));

Den udskriver den lave størrelse som følger:

The shallow size is: 16

5.4. Ukomprimeret størrelse

Hvis vi deaktiverer de komprimerede referencer eller bruger mere end 32 GB af heapen, vil den overfladiske størrelse øges:

Professor object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION                               VALUE
      0    16                       (object header)                           N/A
     16     8                double Professor.lastEvaluation                  N/A
     24     4                   int Professor.level                           N/A
     28     1               boolean Professor.tenured                         N/A
     29     3                       (alignment/padding gap)                  
     32     8      java.lang.String Professor.name                            N/A
     40     8        java.util.List Professor.courses                         N/A
     48     8   java.time.LocalDate Professor.birthDay                        N/A
Instance size: 56 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

Når de komprimerede referencer er deaktiveret, vil objekthovedet og objektreferencerne optage mere hukommelse. Derfor, som vist ovenfor, nu den samme Professor  klasse bruger 16 flere bytes.

5.5. Dyb størrelse

For at beregne den dybe størrelse bør vi inkludere den fulde størrelse af selve objektet og alle dets samarbejdspartnere. For eksempel for dette simple scenario:

String ds = "Data Structures";
Course course = new Course(ds);

Den dybe størrelse af Kurset  instans er lig med den lave størrelse af Kurset  selve instansen plus den dybe størrelse af den pågældende streng  eksempel.

Når det er sagt, lad os se, hvor meget plads den streng  instans bruger:

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

Hver streng  instans indkapsler et char[]  (mere om dette senere) og en int  hashkode:

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               VALUE
      0     4          (object header)                           01 00 00 00 
      4     4          (object header)                           00 00 00 00 
      8     4          (object header)                           da 02 00 f8
     12     4   char[] String.value                              [D, a, t, a,  , S, t, r, u, c, t, u, r, e, s]
     16     4      int String.hash                               0
     20     4          (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Den overfladiske størrelse af denne streng  instans er 24 bytes, som inkluderer de 4 bytes cachelagret hashkode, 4 bytes char[]  reference og andre typiske objektoverhead.

For at se den faktiske størrelse af tegn[],  vi kan også analysere dets klasselayout:

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

Layoutet af char[] ser sådan ud:

[C object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00
      4     4        (object header)                           00 00 00 00
      8     4        (object header)                           41 00 00 f8 
     12     4        (object header)                           0f 00 00 00
     16    30   char [C.<elements>                             N/A
     46     2        (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

Så vi har 16 bytes til Kurset  forekomst, 24 bytes for strengen  instans og til sidst 48 bytes for char[]. I alt den dybe størrelse af det kursus  instans er 88 bytes.

Med introduktionen af ​​kompakte strenge i Java 9, String klasse bruger internt en byte[]  for at gemme tegnene:

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               
      0     4          (object header)                         
      4     4          (object header)                           
      8     4          (object header)                           
     12     4   byte[] String.value # the byte array                             
     16     4      int String.hash                               
     20     1     byte String.coder # encodig                             
     21     3          (loss due to the next object alignment)

Derfor er det samlede fodaftryk for Kurset  på Java 9+ instans vil være 72 bytes i stedet for 88 bytes.

5.6. Objektgraflayout

I stedet for at analysere klasselayoutet for hvert objekt i en objektgraf separat, kan vi bruge GraphLayout. Med GraphLayot,  vi passerer blot startpunktet for objektgrafen, og den vil rapportere layoutet af alle objekter, der kan nås fra dette udgangspunkt. På denne måde kan vi beregne den dybe størrelse af grafens udgangspunkt.

For eksempel kan vi se det samlede fodaftryk for Kurset  eksempel som følger:

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

Som udskriver følgende oversigt:

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

Det er 88 bytes i alt. Den totalSize()  metoden returnerer objektets samlede fodaftryk, som er 88 bytes:

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

6. Instrumentering

For at beregne den lave størrelse af et objekt kan vi også bruge Java-instrumenteringspakken og Java-agenter. Først skal vi oprette en klasse med en premain()  metode:

public class ObjectSizeCalculator {

    private static Instrumentation instrumentation;

    public static void premain(String args, Instrumentation inst) {
        instrumentation = inst;
    }

    public static long sizeOf(Object o) {
        return instrumentation.getObjectSize(o);
    }
}

Som vist ovenfor bruger vi getObjectSize()  metode til at finde den lave størrelse af et objekt. Vi har også brug for en manifestfil:

Premain-Class: com.baeldung.objectsize.ObjectSizeCalculator

Brug derefter denne MANIFEST.MF  fil, kan vi oprette en JAR-fil og bruge den som en Java-agent:

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

Endelig, hvis vi kører en kode med -javaagent:/path/to/agent.jar  argument, så kan vi bruge sizeOf()  metode:

String ds = "Data Structures";
Course course = new Course(ds);

System.out.println(ObjectSizeCalculator.sizeOf(course));

Dette vil udskrive 16 som den lave størrelse af Kurset  eksempel.

7. Klassestatistik

For at se den overfladiske størrelse af objekter i en allerede kørende applikation kan vi tage et kig på klassestatistikken ved hjælp af jcmd:

$ jcmd <pid> GC.class_stats [output_columns]

For eksempel kan vi se hver enkelt forekomsts størrelse og antal af alle kurset  forekomster:

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

Igen, dette rapporterer den overfladiske størrelse af hvert kursus  instans som 16 bytes.

For at se klassestatistikken bør vi starte applikationen med -XX:+UnlockDiagnosticVMOptions  tuning flag.

8. Dyngedump

Brug af heap-dumps er en anden mulighed for at inspicere instansstørrelserne i kørende applikationer. På denne måde kan vi se den bevarede størrelse for hver instans. For at tage et heap-dump kan vi bruge jcmd  som følgende:

$ jcmd <pid> GC.heap_dump [options] /path/to/dump/file

For eksempel:

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

Dette vil oprette et heap-dump på den angivne placering. Også med -alle  mulighed, vil alle tilgængelige og uopnåelige objekter være til stede i heap-dumpen. Uden denne mulighed vil JVM'en udføre en fuld GC før oprettelse af heap-dumpen.

Efter at have hentet heap-dumpet, kan vi importere det til værktøjer som Visual VM:

Som vist ovenfor er den bevarede størrelse af det eneste Kursus  instans er 24 bytes. Som tidligere nævnt kan den bevarede størrelse være et vilkårligt sted mellem lavvandede (16 bytes) og dybe størrelser (88 bytes).

Det er også værd at nævne, at Visual VM var en del af Oracle- og Open JDK-distributionerne før Java 9. Dette er dog ikke længere tilfældet fra Java 9, og vi bør downloade Visual VM'en fra dens hjemmeside separat.

9. Konklusion

I denne tutorial blev vi bekendt med forskellige metrikker til at måle objektstørrelser i JVM-runtime. Derefter målte vi faktisk instansstørrelser med forskellige værktøjer såsom JOL, Java Agents og jcmd  kommandolinjeværktøj.

Som sædvanlig er alle eksemplerne tilgængelige på GitHub.


Java tag