1. 概要

このチュートリアルでは、各オブジェクトがJavaヒープで消費するスペースを確認します。

まず、オブジェクトサイズを計算するためのさまざまなメトリックについて理解します。 次に、インスタンスサイズを測定するいくつかの方法を見ていきます。

通常、ランタイムデータ領域のメモリレイアウトはJVM仕様の一部ではなく、実装者裁量に任されています。 したがって、各JVM実装には、メモリ内のオブジェクトと配列をレイアウトするための異なる戦略がある場合があります。 これは、実行時のインスタンスサイズに影響します。

このチュートリアルでは、特定のJVM実装であるHotSpotJVMに焦点を当てています。

また、チュートリアル全体で、JVMとHotSpotJVMの用語を同じ意味で使用しています。

2. 浅い、保持された、深いオブジェクトサイズ

オブジェクトのサイズを分析するために、浅いサイズ、保持されたサイズ、深いサイズの3つの異なるメトリックを使用できます。

オブジェクトの浅いサイズを計算するときは、オブジェクト自体のみを考慮します。つまり、オブジェクトに他のオブジェクトへの参照がある場合、実際のオブジェクトサイズではなく、ターゲットオブジェクトへの参照サイズのみを考慮します。 。 例えば:

上に示したように、 Triple インスタンスの浅いサイズは、3つの参照の合計にすぎません。 参照されるオブジェクトの実際のサイズ、つまり A1、B1、および C1、はこのサイズから除外されます。

逆に、オブジェクトの深いサイズには、浅いサイズに加えて、参照されているすべてのオブジェクトのサイズが含まれます。

ここでの深いサイズトリプルインスタンスには、3つの参照と実際のサイズが含まれています A1、B1、 C1。 したがって、深いサイズは本質的に再帰的です。

GCがオブジェクトによって占有されているメモリを再利用すると、特定の量のメモリが解放されます。 その量は、そのオブジェクトの保持サイズです。

トリプルインスタンスの保持サイズには、トリプルインスタンス自体に加えて、A1C1のみが含まれます。 一方、この保持サイズには、 B1、 以来ペアインスタンスには、への参照もあります B1。 

これらの追加の参照は、JVM自体によって間接的に行われる場合があります。 したがって、保持サイズの計算は複雑な作業になる可能性があります。

保持されるサイズをよりよく理解するには、ガベージコレクションの観点から考える必要があります。 収集トリプルインスタンスは A1 C1 到達不能ですが、 B1 まだ別のオブジェクトを介して到達可能です。 状況に応じて、保持されるサイズは浅いサイズと深いサイズの間のどこでもかまいません。

3. 依存

JVM内のオブジェクトまたは配列のメモリレイアウトを検査するには、Javaオブジェクトレイアウト( JOL )ツールを使用します。 したがって、jol-core依存関係を追加する必要があります。

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

4. 単純なデータ型

より複雑なオブジェクトのサイズをよりよく理解するには、まず、各単純なデータ型が消費するスペースの量を知る必要があります。これを行うには、JavaメモリレイアウトまたはJOLにVM情報の印刷を依頼できます。 :

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

上記のコードは、単純なデータ型のサイズを次のように出力します。

# 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]

したがって、JVMの各単純データ型のスペース要件は次のとおりです。

  • オブジェクト参照は4バイトを消費します
  • ブールおよびバイト値は1バイトを消費します
  • shortおよびchar値は2バイトを消費します
  • intおよびfloat値は4バイトを消費します
  • longおよびdouble値は8バイトを消費します

これは、32ビットアーキテクチャと、圧縮参照が有効になっている64ビットアーキテクチャにも当てはまります。

また、すべてのデータ型が配列コンポーネント型として使用される場合、同じ量のメモリを消費することにも言及する価値があります。

4.1. 非圧縮の参照

-XX:-UseCompressedOops 調整フラグを使用して圧縮参照を無効にすると、サイズ要件が変更されます。

# 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]

これで、オブジェクト参照は4バイトではなく8バイトを消費します。 残りのデータ型は、引き続き同じ量のメモリを消費します。

さらに、ヒープサイズが32 GBを超える場合、HotSpot JVMも圧縮参照を使用できません(オブジェクトの配置を変更しない限り)。

つまり、圧縮された参照を明示的に無効にするか、ヒープサイズが32 GBを超える場合、オブジェクト参照は8バイトを消費します。

基本的なデータ型のメモリ消費量がわかったので、より複雑なオブジェクトのメモリ消費量を計算してみましょう。

5. 複雑なオブジェクト

複雑なオブジェクトのサイズを計算するために、一般的な教授とコースの関係を考えてみましょう。

public class Course {

    private String name;

    // constructor
}

教授、は、個人情報に加えて、コースのリストを持つことができます。

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. 浅いサイズ:コースクラス

Course クラスインスタンスの浅いサイズには、4バイトのオブジェクト参照( name フィールド用)とオブジェクトオーバーヘッドが含まれている必要があります。 JOLを使用してこの仮定を確認できます。

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

これにより、次のように出力されます。

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

上に示したように、浅いサイズは16バイトで、nameフィールドへの4バイトのオブジェクト参照とオブジェクトヘッダーが含まれます。

5.2. 浅いサイズ:教授クラス

Professor クラスに対して同じコードを実行すると、次のようになります。

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

次に、JOLはProfessorクラスのメモリ消費量を次のように出力します。

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

おそらく予想どおり、カプセル化されたフィールドは25バイトを消費しています。

  • それぞれが4バイトを消費する3つのオブジェクト参照。 したがって、他のオブジェクトを参照するために合計12バイト
  • 4バイトを消費する1つのint
  • 1バイトを消費する1つのブール
  • 8バイトを消費する1つのdouble

オブジェクトヘッダーの12バイトのオーバーヘッドと3バイトの配置パディングを追加すると、浅いサイズは40バイトになります。

ここで重要なポイントは、各オブジェクトのカプセル化された状態に加えて、さまざまなオブジェクトサイズを計算するときに、オブジェクトヘッダーと配置パディングを考慮する必要があることです。

5.3. 浅いサイズ:インスタンス

JOLのsizeOf()メソッドは、オブジェクトインスタンスの浅いサイズを計算するためのはるかに簡単な方法を提供します。 次のスニペットを実行すると、次のようになります。

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

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

浅いサイズは次のように印刷されます。

The shallow size is: 16

5.4. 非圧縮サイズ

圧縮された参照を無効にするか、32 GBを超えるヒープを使用すると、浅いサイズが大きくなります。

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

圧縮参照が無効になっている場合、オブジェクトヘッダーとオブジェクト参照はより多くのメモリを消費します。したがって、上記のように、同じ教授クラスはさらに16バイトを消費します。

5.5. ディープサイズ

ディープサイズを計算するには、オブジェクト自体とそのすべてのコラボレーターのフルサイズを含める必要があります。 たとえば、この単純なシナリオの場合:

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

コースインスタンスの深いサイズは、コースインスタンス自体の浅いサイズにその特定の文字列インスタンスの深いサイズを加えたものに等しくなります。

そうは言っても、Stringインスタンスが消費するスペースを見てみましょう。

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

Stringインスタンスは、 char [] (これについては後で詳しく説明します)とintハッシュコードをカプセル化します。

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

このStringインスタンスの浅いサイズは24バイトで、これには4バイトのキャッシュされたハッシュコード、4バイトの char [] 参照、およびその他の一般的なオブジェクトオーバーヘッドが含まれます。

char []の実際のサイズを確認するために、そのクラスレイアウトを解析することもできます。

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

char[]のレイアウトは次のようになります。

[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

したがって、16バイトがありますコースインスタンス、24バイトインスタンス、そして最後に48バイト char[]。 全体として、その深いサイズコースインスタンスは88バイトです。

Java 9にコンパクト文字列が導入されたことにより、文字列クラスは内部でバイト[]を使用して文字を格納しています。

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)

したがって、Java 9以降では、コースインスタンスの合計フットプリントは88バイトではなく72バイトになります。

5.6. オブジェクトグラフのレイアウト

オブジェクトグラフ内の各オブジェクトのクラスレイアウトを個別に解析する代わりに、 GraphLayout。 GraphLayot、 オブジェクトグラフの開始点を渡すだけで、その開始点から到達可能なすべてのオブジェクトのレイアウトが報告されます。 このようにして、グラフの開始点の深いサイズを計算できます。

たとえば、コースインスタンスの合計フットプリントは次のように表示されます。

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

これは、次の要約を出力します。

Course@67b6d4aed 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)

これは合計88バイトです。 totalSize()メソッドは、オブジェクトの合計フットプリント(88バイト)を返します。

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

6. 計装

オブジェクトの浅いサイズを計算するために、JavaインストルメンテーションパッケージおよびJavaエージェントを使用することもできます。 まず、 premain()メソッドを使用してクラスを作成する必要があります。

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);
    }
}

上に示したように、 getObjectSize()メソッドを使用して、オブジェクトの浅いサイズを見つけます。 マニフェストファイルも必要です。

Premain-Class: com.baeldung.objectsize.ObjectSizeCalculator

次に、この MANIFEST.MF ファイルを使用して、 JARファイルを作成し、それをJavaエージェントとして使用できます。

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

最後に、 -javaagent:/path/to/agent.jar 引数を使用してコードを実行すると、 sizeOf()メソッドを使用できます。

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

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

これにより、コースインスタンスの浅いサイズとして16が出力されます。

7. クラス統計

すでに実行中のアプリケーションのオブジェクトの浅いサイズを確認するには、 jcmd:を使用してクラスの統計を確認できます。

$ jcmd <pid> GC.class_stats [output_columns]

たとえば、すべてのコースインスタンスの各インスタンスサイズと数を確認できます。

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

繰り返しになりますが、これは各コースインスタンスの浅いサイズを16バイトとして報告しています。

クラスの統計を確認するには、 -XX:+UnlockDiagnosticVMOptions調整フラグを使用してアプリケーションを起動する必要があります。

8. ヒープダンプ

ヒープダンプを使用することは、実行中のアプリケーションのインスタンスサイズを検査するためのもう1つのオプションです。 このようにして、各インスタンスの保持サイズを確認できます。 ヒープダンプを取得するには、jcmdを次のように使用できます。

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

例えば:

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

これにより、指定した場所にヒープダンプが作成されます。 また、 -all オプションを使用すると、到達可能および到達不可能なすべてのオブジェクトがヒープダンプに存在します。 このオプションがないと、JVMはヒープダンプを作成する前に完全なGCを実行します。

ヒープダンプを取得したら、それをVisualVMなどのツールにインポートできます。

上に示したように、コースインスタンスのみの保持サイズは24バイトです。 前述のように、保持されるサイズは、浅いサイズ(16バイト)と深いサイズ(88バイト)の間のどこでもかまいません。

VisualVMはJava9より前のOracleおよびOpenJDKディストリビューションの一部であったことにも言及する価値があります。 ただし、これはJava 9以降では当てはまらないため、VisualVMをWebサイトから個別にダウンロードする必要があります。

9. 結論

このチュートリアルでは、JVMランタイムでオブジェクトサイズを測定するためのさまざまなメトリックについて理解しました。 その後、実際には、JOL、Javaエージェント、jcmdコマンドラインユーティリティなどのさまざまなツールを使用してインスタンスサイズを測定しました。

いつものように、すべての例はGitHubから入手できます。