Java >> Java チュートリアル >  >> JVM

JVM でのオブジェクト サイズの測定

1.概要

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

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

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

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

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

2.浅い、保持、および深いオブジェクト サイズ

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

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

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

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

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

オブジェクトが占有していたメモリを GC が回収すると、特定の量のメモリが解放されます。その量は、そのオブジェクトの保持サイズです:

トリプルの保持サイズ インスタンスには A1 のみが含まれます および C1 トリプルに加えて インスタンス自体。一方、この保持されたサイズには 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 バイトを消費します
  • 短い文字 値は 2 バイトを消費します
  • 整数 そして浮かぶ 値は 4 バイトを消費します
  • 長い そしてダブル 値は 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 バイトを消費するようになりました。残りのデータ型は、同じ量のメモリを消費します。

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

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

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

5.複雑なオブジェクト

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

public class Course {

    private String name;

    // constructor
}

教授 個人情報に加えて、コースのリストを持つことができます 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.浅いサイズ:コース クラス

コースの浅いサイズ クラス インスタンスには、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 バイトを消費しています:

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

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

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

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

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

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

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

5.5.ディープサイズ

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

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

コースの深いサイズ インスタンスはコースの浅いサイズに等しい インスタンス自体とその特定の String の深いサイズ インスタンス。

そうは言っても、文字列がどのくらいのスペースか見てみましょう インスタンスが消費する:

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

文字列 インスタンスは 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

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

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 バイト インスタンス、最後に char[] の 48 バイトです。 合計で、そのコースの深いサイズ インスタンスは 88 バイトです。

Java 9 でのコンパクト文字列の導入により、 String クラス は byte[] を内部的に使用しています 文字を保存するには:

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

次の要約が表示されます:

[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)

合計で 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.ヒープダンプ

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

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

例:

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

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

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

上記のように、コースのみの保持サイズ インスタンスは 24 バイトです。前述のように、保持されるサイズは、浅いサイズ (16 バイト) と深いサイズ (88 バイト) の間の任意のサイズにすることができます。

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

9.結論

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

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


Java タグ