JVMでのオブジェクトサイズの測定
1. 概要
このチュートリアルでは、各オブジェクトがJavaヒープで消費するスペースを確認します。
まず、オブジェクトサイズを計算するためのさまざまなメトリックについて理解します。 次に、インスタンスサイズを測定するいくつかの方法を見ていきます。
通常、ランタイムデータ領域のメモリレイアウトはJVM仕様の一部ではなく、実装者の裁量に任されています。 したがって、各JVM実装には、メモリ内のオブジェクトと配列をレイアウトするための異なる戦略がある場合があります。 これは、実行時のインスタンスサイズに影響します。
このチュートリアルでは、特定のJVM実装であるHotSpotJVMに焦点を当てています。
また、チュートリアル全体で、JVMとHotSpotJVMの用語を同じ意味で使用しています。
2. 浅い、保持された、深いオブジェクトサイズ
オブジェクトのサイズを分析するために、浅いサイズ、保持されたサイズ、深いサイズの3つの異なるメトリックを使用できます。
オブジェクトの浅いサイズを計算するときは、オブジェクト自体のみを考慮します。つまり、オブジェクトに他のオブジェクトへの参照がある場合、実際のオブジェクトサイズではなく、ターゲットオブジェクトへの参照サイズのみを考慮します。 。 例えば:
上に示したように、 Triple インスタンスの浅いサイズは、3つの参照の合計にすぎません。 参照されるオブジェクトの実際のサイズ、つまり A1、B1、および C1、はこのサイズから除外されます。
逆に、オブジェクトの深いサイズには、浅いサイズに加えて、参照されているすべてのオブジェクトのサイズが含まれます。
ここでの深いサイズトリプルインスタンスには、3つの参照と実際のサイズが含まれています A1、B1、 と
GCがオブジェクトによって占有されているメモリを再利用すると、特定の量のメモリが解放されます。 その量は、そのオブジェクトの保持サイズです。
トリプルインスタンスの保持サイズには、トリプルインスタンス自体に加えて、A1とC1のみが含まれます。 一方、この保持サイズには、 B1、 以来ペアインスタンスには、への参照もあります
これらの追加の参照は、JVM自体によって間接的に行われる場合があります。 したがって、保持サイズの計算は複雑な作業になる可能性があります。
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バイト
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. オブジェクトグラフのレイアウト
オブジェクトグラフ内の各オブジェクトのクラスレイアウトを個別に解析する代わりに、
たとえば、コースインスタンスの合計フットプリントは次のように表示されます。
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でから入手できます。