オブジェクトのメモリレイアウトと参照カウント
64ビットシステムにおけるオブジェクト構造
Objective-Cのオブジェクトは、内部的には以下のような構造体で表現されます。
struct objc_object {
isa_union_t isa;
};
ここでの isa は単なるクラスへのポインタではなく、ビットフィールド(位域)を利用した最適化された構造体です。これにより、クラス情報のみならずオブジェクトのステータスや参照カウントの一部を直接保持することが可能になっています。
isaビットフィールドの分解
| ビット領域 | 役割と詳細 |
|---|---|
is_optimized |
非ポインタisa(最適化されたランタイム)であるかのフラグ |
has_associated_objects |
関連オブジェクト(Associated Objects)が紐づいているか |
requires_cxx_destruction |
C++のデストラクタ呼び出しが必要か |
class_memory_address |
クラスオブジェクトのメモリポインタ |
runtime_validation |
ランタイムの整合性を検証するためのマジックナンバー |
has_weak_references |
weakポインタから参照されているか |
is_deallocating |
現在解放処理中であるかのフラグ |
overflow_to_sidetable |
参照カウントがisaの容量を超え、SideTableに溢れているか |
inline_reference_count |
isa内に直接埋め込まれた参照カウント(arm64では通常19ビット) |
設計思想:
- 高速パス: 参照カウントの大部分は
inline_reference_countに保持し、ロック不要で高速にアクセスします。 - 低速パス: カウントがオーバーフローした場合や、weak/関連オブジェクトが存在する場合は、グローバルな
SideTableを利用します。
グローバルハッシュテーブル「SideTable」の仕組み
SideTableのデータ構造
SideTableはランタイムによって管理されるグローバルな構造体であり、以下のようなコンポーネントで構成されています。
struct RuntimeSideTable {
os_unfair_lock table_lock; // スレッドセーフを担保するロック
RefcountMap overflow_counts; // isaから溢れた参照カウントを格納
WeakReferenceMap weak_refs; // weakポインタのアドレスを管理
AssociatedObjectMap associated; // 関連オブジェクトの辞書
};
RefcountMap: オブジェクトのポインタをキーとし、溢れた参照カウントを値とするハッシュマップ。WeakReferenceMap: オブジェクトをキーとし、そのオブジェクトを指すすべてのweakポインタのアドレスリストを値とします。AssociatedObjectMap: 関連オブジェクトを格納する辞書。
SideTableが利用されるケース
| トリガー | 理由 |
|---|---|
| 参照カウントのオーバーフロー | isaのビット数(19ビット)では表現しきれないため |
| weak参照の存在 | weakポインタの一元管理と、解放時の自動nil化のため |
| 関連オブジェクトの存在 | カテゴリなどで追加されたプロパティの情報を保持するため |
ハッシュ戦略とパフォーマンス最適化
SideTableへのアクセスはハッシュ関数によってバケットが計算されます。衝突時にはオープンアドレス法やチェーン法が適用されます。
重要な最適化として、Appleは StripedMap を採用しています。これは単一のグローバルSideTableではなく、複数のSideTableに分割(シャーディング)したものです。
static StripedMap<RuntimeSideTable> GlobalSideTables;
もし単一のテーブルだった場合、すべてのオブジェクトの参照カウント操作でグローバルロックの競合が発生し、パフォーマンスが著しく低下します。分片化(シャーディング)により、各テーブルが独立したロックを持つため、マルチスレッド環境での競合を大幅に削減しています。ただし、isa内部の参照に比べると、SideTableを経由する weak 参照のアクセスにはロックのオーバーヘッドが伴います。
retain と release の実行フロー
retainのロジック
id custom_objc_retain(id target_instance) {
if (target_instance == NULL) return NULL;
if (target_instance->isa.is_optimized) {
// 高速パス: isa内のカウントをCASでインクリメント
if (!attempt_inline_increment(target_instance)) {
// 低速パス: 溢れた場合はSideTableをロックして更新
increment_via_sidetable(target_instance);
}
} else {
// 非最適化isa: 直接SideTableで管理
increment_via_sidetable(target_instance);
}
return target_instance;
}
ここで利用される CAS (Compare-And-Swap) は、CPUレベルで提供されるアトミックな命令です。メモリ上の値が期待値と一致する場合のみ、新しい値に置き換えます。この操作は不可分であり、マルチスレッド環境でもロック(mutex)を使用せずに安全にデータを同期できます。
releaseのロジック
void custom_objc_release(id target_instance) {
if (target_instance == NULL) return;
if (target_instance->isa.is_optimized) {
if (!attempt_inline_decrement(target_instance)) {
if (!decrement_via_sidetable(target_instance)) {
trigger_deallocation(target_instance);
}
}
} else {
if (!decrement_via_sidetable(target_instance)) {
trigger_deallocation(target_instance);
}
}
}
参照カウントが0になった場合、trigger_deallocation が呼び出される前に、SideTable上のweak参照のクリアや関連オブジェクトの破棄といった後処理が実行されます。
weakポインタの裏側:weakテーブル
weak_tableの構造
struct WeakHashTable {
WeakEntryNode** hash_buckets;
size_t bucket_capacity;
os_unfair_lock table_mutex;
};
struct WeakEntryNode {
DisguisedPtr<objc_object> target_object;
WeakPointerAddressList* pointing_addresses;
};
各 WeakEntryNode は、参照対象のオブジェクトと、そのオブジェクトを指しているすべてのweakポインタのメモリアドレスを保持しています。
weakポインタの登録プロセス
__weak id w = obj; が実行されると、以下のステップで登録が行われます。
objのNULLチェック。WeakHashTable内から対象のWeakEntryNodeを検索、または新規作成。- weakポインタ
wのアドレスをpointing_addressesリストに追加。 - 対象オブジェクトのisaフラグ
has_weak_referencesを1に設定。
この登録処理もCASを用いてスレッドセーフに実行されます。
オブジェクト解放とweakの自動nil化
custom_objc_releaseにより参照カウントが0であることを検知。- isaの
has_weak_referencesフラグを確認。 - フラグが立っている場合、
pointing_addressesを走査し、登録されているすべてのweakポインタの値をアトミックにNULL(nil)で書き換え。 WeakEntryNodeをテーブルから削除。- 最終的に
deallocを実行。
AutoreleasePoolとARCの連携
autorelease メソッドが呼び出されると、オブジェクトは現在のスレッドに紐づく AutoreleasePoolPage という双方向リンクリストのスタックに追加されます。これにより、即座に解放されるのではなく、リリース処理が遅延されます。RunLoopのイテレーション終了時や、プールが明示的にdrainされたタイミングで、スタックに積まれたオブジェクトが一括して release されます。
オブジェクトのライフサイクル概要
- オブジェクト生成
- retain / release 操作は
isa.inline_reference_countで処理 - カウント超過 →
SideTable.overflow_countsへ移行 - weak参照の発生 →
SideTable.weak_refsへアドレス登録 - 関連オブジェクトの付与 →
SideTable.associatedへ格納
- retain / release 操作は
- 参照カウント == 0 の検知
- weakテーブルを走査し、関連するweakポインタをすべてnil化
- 関連オブジェクトを破棄
- メモリ解放(dealloc)を実行