Javaオブジェクトのデータ構造の詳しい説明

私は、一定期間学習した後、Java オブジェクトに何が格納されているかについて混乱しました。今日はまとめに来てください。これが見た人の助けになれば幸いです。

1. 全体概要

HotSpot 仮想マシンでは、メモリに格納されているオブジェクトのレイアウトは、オブジェクト ヘッダー (Header)、インスタンス データ (Instance Data)、および位置合わせパディング (Padding) の3 つの領域に分割できます。次の図は、共通オブジェクト インスタンスと配列オブジェクト インスタンスのデータ構造を示しています。
ここに画像の説明を挿入

オブジェクトヘッダー

HotSpot 仮想マシンのオブジェクト ヘッダーには、次の 2 つの部分の情報が含まれます。

  1. マークワード
    マークワードこれは、ハッシュ コード (HashCode)、GC 世代経過時間、ロック ステータス フラグ、スレッドが保持するロック、偏ったスレッド ID、偏ったタイムスタンプなど、オブジェクト自体の実行時データを格納するために使用されます。データは 32 ビット、64 ビットの仮想マシン (圧縮ポインタがオンになっていない) はそれぞれ 32 ビット、64 ビットであり、正式名称は「MarkWord」です。
  2. クラスワード
    クラスワードこれはクラス タイプ ポインター、つまりクラス メタデータへのオブジェクトのポインターであり、仮想マシンはこのポインターを使用してオブジェクトがどのクラスのインスタンスであるかを判断します。
  3. 配列の長さ (配列オブジェクトのみ):
    オブジェクトが配列の場合、オブジェクト ヘッダーに配列の長さを記録するデータが必要です。

インスタンスデータ

  • インスタンスデータこれはオブジェクトが実際に格納する有効な情報であり、プログラムコード内で定義された各種フィールドの内容でもあります。親クラスから継承されたものであっても、サブクラスで定義されたものであっても、記録する必要があります。

パディングを揃える

  • パディングを揃えるこれは必ずしも存在するわけではなく、特別な意味もありません。単にプレースホルダーとして機能します。HotSpot VM の自動メモリ管理システムでは、オブジェクトの開始アドレスが 8 バイトの整数倍である必要があるため、言い換えれば、オブジェクトのサイズは 8 バイトの整数倍である必要があります。オブジェクトのヘッダー部分はちょうど 8 バイトの倍数 (1 倍または 2 倍) であるため、オブジェクト インスタンスのデータ部分がアライメントされていない場合は、アライメント パディングによって補完する必要があります。(実際にはゴミを減らすのが主な目的です)

より深く理解する

オブジェクトの全体的な構造を理解したところで、オブジェクト ヘッダーの 3 つの部分を詳しく見てみましょう。

マークワード

オブジェクト ヘッダー マークワードは、openjdk のソース コードに次のように記述されています。
ここに画像の説明を挿入
ソース コードの内容を変換して、次のアイコン形式を取得します。その後、このテーブルについて詳しく説明します。
ここに画像の説明を挿入
このテーブルの各行は、オブジェクトが特定の状態でどのように見えるかを示しています。

lock: 2 ビットのロック ステータス フラグ。できるだけ多くの情報を表すためにできるだけ少ないバイナリ ビットを使用することが望まれるため、ロック フラグが設定されます。マークの価値が異なり、マークワード全体の意味も異なります。biased_lock と lock は一緒にロック状態の意味を次のように表します:
biased_lock: オブジェクトがバイアス ロック フラグで有効かどうか、バイナリ ビット 1 つだけを占有するかどうか。1 の場合、オブジェクトのバイアス ロックが有効になっていることを意味し、0 の場合、オブジェクトにバイアス ロックがないことを意味します。lock とbiased_lock は共に、オブジェクトがどのようなロック状態にあるかを示します。

age: 4 桁の Java オブジェクトの年齢。GC では、オブジェクトが Survivor 領域に 1 回コピーされると、経過時間が 1 増加します。オブジェクトが設定されたしきい値に達すると、古い世代に昇格します。デフォルトでは、並列 GC の経過時間のしきい値は 15 で、同時 GC の経過時間のしきい値は 6 です。age には 4 ビットしかないため、最大値は 15 です。そのため、-XX:MaxTenuringThreshold オプションの最大値は 15 です。

identity_hashcode: 遅延読み込みテクノロジーを使用した 31 ビットのオブジェクト識別子ハッシュコード。System.identityHashCode() メソッドを呼び出して計算し、結果をオブジェクト ヘッダーに書き込みます。オブジェクトがロックされている場合 (バイアス、軽量、重量)、MarkWord のバイトにはハッシュコードを保存するための十分なスペースがないため、値はモニターに移動されます。

thread: バイアスされたロックを保持しているスレッドの ID。

epoch: バイアスされたロックのタイムスタンプ。

ptr_to_lock_record: 軽量ロック状態では、スタック内のロック レコードへのポインタ。

ptr_to_heavyweight_monitor: ヘビーウェイト ロック状態では、オブジェクト モニター Monitor へのポインター。

より直感的なイメージを残しましょう。
ここに画像の説明を挿入
synchronized によって実装される同期ロックの実際の名前は、ヘビーウェイト ロックと呼ばれます。ただし、重いロックを使用すると、スレッドがキューイング (シリアル実行) され、CPU がユーザー モードとコア モードを頻繁に切り替えるため、コストが高く効率が低くなります。効率を向上させるために、重いロックは最初から使用されず、必要に応じて JVM が内部でロックをアップグレードします。

ここではまず、これらのタイプのロックについて説明します。

ロックの分類:

  1. ロック
    フリー ロックフリーとは、リソースがロックされておらず、すべてのスレッドが同じリソースにアクセスして変更できるが、同時に正常に変更できるのは 1 つのスレッドだけであることを意味します。
    ロックフリーの特徴は、変更操作がループ内で実行され、スレッドが共有リソースの変更を試行し続けることです。競合がない場合、変更は成功して終了します。競合しない場合はループが継続します。同じ値を変更するスレッドが複数ある場合、その値を正常に変更できるスレッドが 1 つある必要がありますが、変更に失敗した他のスレッドは変更が成功するまで再試行を続けます。
  2. バイアスされたロック
    同期されたコード ブロックが初めて実行されると、ロック オブジェクトはバイアスされたロックになります (オブジェクト ヘッダーのロック フラグは CAS を通じて変更されます)。これは文字通り、「最初に取得したスレッドにバイアスされたロック」を意味します。それ"。同期コード ブロックの実行後、スレッドはバイアス ロックを積極的に解放しません。2 回目に同期コード ブロックに到達すると、スレッドはロックを保持しているスレッドがそれ自体であるかどうかを判断し (ロックを保持しているスレッド ID はオブジェクト ヘッダーにもあります)、そうであれば通常どおり実行を続けます。以前にロックが解除されていないため、ここで再度ロックする必要はありません。最初から最後までロックを使用するスレッドが 1 つだけであれば、ロックをバイアスするための追加のオーバーヘッドがほとんどなく、パフォーマンスが非常に高いことが明らかです。バイアスされたロックとは、同期コードの一部が常に同じスレッドによってアクセスされる場合、つまり、複数のスレッド間で競合がない場合、スレッドは後続のアクセス中に自動的にロックを取得し、それによってロック取得のコストが削減されることを意味します。 、つまりパフォーマンスの向上です。スレッドが同期されたコード ブロックにアクセスしてロックを取得すると、ロック バイアスされたスレッド ID がマーク ワードに保存されます。スレッドが同期ブロックに出入りするとき、ロックとロック解除に CAS 操作は使用されなくなり、マーク ワードに格納されている現在のスレッドを指すバイアス ロックがあるかどうかが検出されます。軽量ロックの取得と解放は複数の CAS アトミック命令に依存しますが、バイアスされたロックは ThreadID を置き換えるときに 1 つの CAS アトミック命令にのみ依存する必要があります。他のスレッドがバイアスされたロックをめぐって競合しようとした場合にのみ、バイアスされたロックを保持しているスレッドがロックを解放し、スレッドが積極的にバイアスされたロックを解放することはありません。バイアスされたロックのキャンセルに関しては、グローバル セキュリティ ポイントを待つ必要があります。つまり、ある時点でバイトコードが実行されていない場合、まずバイアスされたロックを所有するスレッドを一時停止し、その後決定します。ロック オブジェクトがロックされているかどうか。スレッドがアクティブでない場合は、オブジェクト ヘッダーをロック フリー状態に設定し、バイアス ロックを解除して、ロック フリー (フラグ ビットが 01) または軽量ロック (フラグ ビットが 00) 状態に戻ります。
  3. **軽量ロック (スピン ロック)**
    ここに画像の説明を挿入軽量ロックとは、ロックがバイアスされたロックである場合に、別のスレッドからアクセスされることを意味します。このとき、バイアスされたロックは軽量ロックにアップグレードされ、他のスレッドが試行されます。スピンの形式でロックを取得するため (スピンの概要については記事の最後を参照)、スレッドがブロックされないため、パフォーマンスが向上します。
    軽量ロックの取得は、主に 2 つの状況によって発生します:
    ① 偏ったロック機能がオフになっている場合、
    ② 複数のスレッドが偏ったロックを競合しているため、偏ったロックが軽量なロックにアップグレードされる。
    2 番目のスレッドがロック競合に参加すると、バイアスされたロックは軽量ロック (スピン ロック) にアップグレードされます。ここで、ロックの競合とは何かを明確にする必要があります。複数のスレッドが順番にロックを取得するが、毎回ロックを取得するのがスムーズでブロックが発生しない場合、ロックの競合は存在しません。ロックの競合は、スレッドがロックを取得しようとしたときに、そのロックがすでに占有されており、解放されるのを待つしかないことが判明した場合にのみ発生します。
    軽量ロック状態では、ロックの競合が継続し、ロックを取得できなかったスレッドがスピンします。つまり、ロックが正常に取得できるかどうかを判断するためにループし続けます。ロックを取得する操作は、実際には、CAS を介してオブジェクト ヘッダー内のロック フラグを変更することです。まず、現在のロック フラグが「解放」されているかどうかを比較し、解放されている場合は「ロック」に設定します。比較と設定はアトミックに行われます。これはロックを取得したと見なされ、スレッドは現在のロック所有者の情報をそれ自体に変更します。長期的なスピン操作は非常にリソースを大量に消費します。1 つのスレッドがロックを保持すると、他のスレッドはその場で CPU を消費することしかできず、効果的なタスクを実行できません。この現象はビジー待機と呼ばれます。複数のスレッドがロックを使用しているが、ロックの競合が発生しない、またはごくわずかなロックの競合が発生する場合、synchronized は軽量のロックを使用して、短期的なビジー状態を許容します。これは、ユーザー モードとカーネル モードの間でスレッドを切り替えるオーバーヘッドと引き換えに、短期間のビジー待機を行うという妥協案です。
  4. ヘビーウェイト ロック
    ヘビーウェイト ロック 明らかに、このビジー待機には制限があります (スピン数を記録するカウンターがあり、デフォルトで 10 サイクルが許可されますが、これは仮想マシン パラメーターを通じて変更できます)。ロックの競合が深刻な場合、スピンの最大数に達したスレッドは軽量ロックを重量ロックにアップグレードします (CAS は引き続きロック フラグを変更しますが、ロックを保持しているスレッド ID は変更しません)。後続のスレッドがロックを取得しようとして、占有されているロックが重量ロックであることが判明すると、(ビジー待機の代わりに) 自身を直接一時停止し、将来ウェイクアップされるのを待ちます。重量ロックとは、スレッドがロックを取得すると、ロックの取得を待機している他のすべてのスレッドがブロックされることを意味します。つまり、すべての制御権はオペレーティング システムに与えられ、オペレーティング システムはスレッド間のスケジューリングとスレッドの状態の変更を担当します。また、これによりスレッドの実行状態が頻繁に切り替わり、スレッドが一時停止したりウェイクアップしたりするため、多くのシステム リソースが消費されます。

そうは言っても、誰もがロックのアップグレードについて一定の理解を持っているはずだと思います。

Klass Word (クラスポインタ)

この部分は、クラス メタデータを指すオブジェクトの型ポインタを格納するために使用され、JVM はこのポインタを使用して、オブジェクトがどのクラスのインスタンスであるかを判断します。ポインタのビット長は JVM のワード サイズです。つまり、32 ビット JVM は 32 ビット、64 ビット JVM は 64 ビットです。
アプリケーション内のオブジェクトが多すぎる場合、64 ビット ポインターを使用すると大量のメモリが浪費され、統計的には、64 ビット JVM は 32 ビット JVM よりも 50% 多くのメモリを消費します。メモリを節約するために、+UseCompressedOops オプションを使用してポインター圧縮を有効にすることができます。ここで、oop は通常のオブジェクト ポインターです。このオプションをオンにすると、次のポインターが 32 ビットに圧縮されます。

各クラスのプロパティ ポインタ (静的変数)
各オブジェクトのプロパティ ポインタ (オブジェクト変数) 通常
のオブジェクト配列の各要素ポインタ
もちろん、すべてのポインタが圧縮されるわけではありません。一部の特殊なタイプのポインタは JVM によって最適化されません。たとえば、PermGen を指す Class オブジェクト ポインター (JDK8 ではメタスペースを指す Class オブジェクト ポインター)、ローカル変数、スタック要素、入力パラメーター、戻り値、NULL ポインターなどです。

配列の長さ

オブジェクトが配列の場合、オブジェクト ヘッダーには配列の長さを格納するための追加スペースが必要です。データのこの部分の長さは JVM アーキテクチャによって異なります。32 ビット JVM では長さは 32 ビットですが、JVM では長さは 32 ビットです。 64 ビット JVM は 64 ビットです。64 ビット JVM で +UseCompressedOops オプションが有効になっている場合、領域の長さも 64 ビットから 32 ビットに圧縮されます。

オブジェクトサイズの計算

要点

  1. 32 ビット システムでは、クラス ポインターを格納するスペースは 4 バイト、MarkWord は 4 バイト、オブジェクト ヘッダーは 8 バイトです。
  2. 64 ビット システムでは、クラス ポインターを格納するためのスペースは 8 バイト、MarkWord は 8 バイト、オブジェクト ヘッダーは 16 バイトです。
  3. 64 ビットでポインター圧縮が有効な場合、クラス ポインターを格納するためのスペースは 4 バイト、MarkWord は 8 バイト、オブジェクト ヘッダーは 12 バイトです。配列長 4 バイト + 配列オブジェクト ヘッダー 8 バイト (オブジェクト参照 4 バイト (64 ビット ポインター圧縮が有効でない場合は 8 バイト) + 配列マークは 4 バイト (64 ビット ポインター圧縮が有効でない場合は 8 バイト)) + アラインメント4 = 16 バイト。
  4. 静的プロパティはオブジェクトのサイズにはカウントされません。

おすすめ

転載: blog.csdn.net/weixin_44021296/article/details/118244047