HotSpot JVMは、オブジェクトの移動にわたるObject.hashCode()の一貫性を、通常、初期のメモリアドレスから計算された値を一度だけ計算し、その結果をガーベジコレクションサイクルがオブジェクトを移動させる前にオブジェクトヘッダーのマークワードにキャッシュすることで保証します。このマークワードには、ハッシュコードフィールドに加えて、ハッシュが具現化されたことを示すビットフラグが含まれており、そのため、後続の呼び出しでは再計算することなくキャッシュされた値が取得されます。したがって、G1やZGCのようなコレクタがオブジェクトを新しいアドレスに移動した際にも、アイデンティティハッシュは物理ポインタから切り離され、変更不可能なヘッダメタデータに保存されるため、安定しています。
分散型ウェブアプリケーションは、IdentityHashMapを利用して、複数のアプリケーションノードでアクティブなSessionオブジェクトを追跡し、ロードバランシング操作中にキャッシュ親和性のルーティングのために**System.identityHashCode()**に依存していました。ピークトラフィック時には、ZGCの低レイテンシコレクタが、緊密なポーズタイム目標を維持するために、若い世代のオブジェクトを頻繁に同時移動させていました。移動時にアイデンティティハッシュが変わってしまった場合、セッションの親和性が崩れ、リクエストがノード間で漏れ出し、一貫性の保証が破られることになります。
アプローチの一つは、作成時に各SessionのためのUUIDインスタンスを生成し、別途**ConcurrentHashMap<UUID, Session>**を維持することでした。利点: JVMオブジェクトのライフサイクルと移動メカニズムからの完全な独立性。欠点: セッションオブジェクトごとに16バイトのオーバーヘッドが追加され、UUID生成からのアロケーション圧力が生じ、バーストトラフィック時にはアロケーションレートが飽和する可能性があります。
チームは、JNIのクリティカルリファレンスを使用して、メモリ内のセッションオブジェクトをピン留めし、GCの移動を防ぐことを検討しました。利点: 安定したメモリアドレスを保証し、したがってアドレスから導出されるアイデンティティハッシュも安定します。欠点: ZGC内の全てのヒープ領域をピン留めし、断片化を引き起こし、コレクタの同時移動能力を妨げることになり、許容できないポーズタイムが発生します。
選択した解決策は、アイデンティティハッシュコードが一定であるというJVMの仕様保証と、HotSpotのマークワードキャッシング実装を活用しました。利点: 追加のメモリオーバーヘッドがゼロで、アロケーションコストがなく、ZGCのような攻撃的なコレクタとの完全な互換性があります。欠点: 規格に文書化されているとはいえ、JVMの実装の詳細に対する信頼が必要です。
アプリケーションは、ピン留めや補助的な識別子を使用せずに完璧なセッション親和性を維持し、数百万のZGCサイクルを経てもサブミリセカンドのポーズタイムを実現し、IdentityHashMapの整合性を保ちました。
**System.identityHashCode()**は、常にオブジェクトの現在のメモリアドレスを整数として返しますか?
いいえ。初期計算はメモリアドレスをエントロピーとして使用するかもしれませんが、結果はオブジェクトヘッダーに即座に保存され、その後は決して変わることはありません。これは、返される整数がGC移動後のオブジェクトの現在の位置を反映せず、開発者はそれをポインタやメモリアドレスのプローブとして扱ってはいけないことを意味します。
アイデンティティハッシュコードは負になる可能性がありますか?コレクションはこれをどのように扱いますか?
はい、32ビット整数値はすべて有効で、負の数も含まれます。IdentityHashMapは、(h ^ (h >>> 16)) & (length-1)のようなマスキング操作を通じて負のハッシュを処理し、Math.abs()は整数の二の補数オーバーフローのためにInteger.MIN_VALUEで失敗するため回避されます。
アイデンティティハッシュコードはすべてのオブジェクトにわたって一意であることが保証されていますか?
いいえ。32ビット整数空間はヒープアドレス空間の潜在サイズよりも小さいため、衝突が可能です。HotSpotは、マルサリアのxorシフトスキームやアドレスベースのハッシュ化を使用して値を良く分散させますが、一意性は保証されておらず、IdentityHashMapはハッシュコードだけでなく参照の等価性に依存して曖昧さを解消します。