Closed7
100日チャレンジ day57 (GCエミュレーター)

昨日
100日チャレンジに感化されたので、アレンジして自分でもやってみます。
やりたいこと
- 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
- 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
- 生成aiをつかったバイブコーティングになれる
- 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)
できたもの

ガベージコレクション のエミュレーターを作る

なるほど!画面上の4つのボタンの機能について概念的に説明しますね。
1. Trigger Young GC(若い世代のガベージコレクション)
- 対象: 新しく作られたオブジェクトが存在する「Young Generation」のみ
- 動作: 短時間生存したオブジェクトを重点的にクリーンアップ
-
特徴:
- 高速実行(範囲が限定的)
- 頻繁に実行される
- 長生きしたオブジェクトはOld Generationに昇格
- 実世界例: 「今日作った書類の中で不要なものだけを捨てる」
2. Trigger Full GC(フルガベージコレクション)
- 対象: Young + Old Generation 全体
- 動作: メモリ全体を徹底的にクリーンアップ
-
特徴:
- 実行時間が長い
- メモリを最大限回収
- アプリケーションの一時停止を伴う場合がある
- 実世界例: 「家全体の大掃除」
3. Clear Root Objects(ルートオブジェクトのクリア)
- 対象: GCの起点となる「ルートセット」
- 動作: 参照の起点をリセット
-
特徴:
- すべてのオブジェクトが到達不可能になる
- 次回GC実行時に全オブジェクトが回収対象
- 実世界例: 「重要書類の目録を全部消す → 何が重要かわからなくなる」
4. Force Mark-and-Sweep(強制マーク・アンド・スイープ)
- 対象: メモリ全体
- 動作: クラシックなGCアルゴリズムを強制実行
-
特徴:
- ルートから辿れるオブジェクトを「マーク」
- マークされていないオブジェクトを「スイープ」(削除)
- 世代に関係なく実行
- 実世界例: 「使っているものに印をつけて、印のないものは全部捨てる」
ガベージコレクションの基本概念
- 目的: 不要になったメモリを自動回収
- 仕組み: プログラムから参照されなくなったオブジェクトを検出・削除
- 効果: メモリリークを防ぎ、アプリケーションの安定動作を保つ
これらのボタンで、異なるGCストラテジーの効果や動作タイミングを体験できるようになっています!

Young Generationを生き残ってOld Generationに昇格する方法について説明しますね!
Young Generation 生き残り戦略
1. 参照され続ける
// 🟢 生き残る例
const longLivedObject = { data: "重要なデータ" };
window.myGlobalRef = longLivedObject; // グローバル参照で保護
// 🔴 死ぬ例
function createTemp() {
const temp = { data: "一時的" };
return; // tempは参照されなくなる → GCで回収
}
2. 複数回のYoung GCを乗り越える
オブジェクト誕生 → Young GC (1回目) → 生存
→ Young GC (2回目) → 生存
→ Young GC (3回目) → 生存
→ 昇格! → Old Generation入り
3. ルートセットからの到達可能性を維持
- ルートセットから辿れる参照チェーンに含まれる
- 例:グローバル変数、スタック上の変数、静的変数など
実際の生存パターン
📈 長寿命パターン
// キャッシュデータ
const cache = new Map();
cache.set("user1", userData); // 長期間参照される
// イベントリスナー
button.addEventListener('click', handler); // handlerが長生き
// クロージャー
function createCounter() {
let count = 0; // countが長生き
return () => ++count;
}
⚡ 短命パターン
// 一時的な計算
const temp = array.map(x => x * 2).filter(x => x > 10);
// → mapの中間結果は即座に不要
// ループ内の一時変数
for(let i = 0; i < 1000; i++) {
const temp = process(data[i]); // 毎回作っては捨てる
}
昇格の条件(実装による)
年齢ベース
- Young GCを N回生き残る(例:3回)
- "age" カウンターが閾値を超える
サイズベース
- Young Generation が満杯に近づく
- 大きなオブジェクトは早期昇格
参照パターンベース
- 他のOld Generationオブジェクトから参照される
- 安定した参照関係を持つ
🎯 生き残りのコツ
- 長期間必要なデータは早めにグローバルスコープで参照
- キャッシュやシングルトンパターンの活用
- イベントハンドラーやコールバック関数の適切な管理
- クロージャーで状態を保持
つまり、「長く使われ続けるオブジェクト」だけがYoung Generationを卒業できるということですね!

良い質問ですね!「1回では死なないけど、数回で死ぬ」オブジェクトの具体例を説明します。
2〜3回のGCで死ぬオブジェクトのパターン
1. 非同期処理の中間データ
async function processData() {
const tempResult = await fetchData(); // GC 1回目: 生存(まだ使用中)
await delay(1000); // 1秒待機
// この間にGC 2回目が走る → tempResultはまだ生存
const finalResult = transform(tempResult);
return finalResult;
// GC 3回目 → tempResultは不要になって回収
}
2. イベント処理の一時状態
let dragState = null;
element.addEventListener('mousedown', (e) => {
dragState = { // GC 1回目: 生存
startX: e.clientX,
startY: e.clientY,
element: e.target
};
});
element.addEventListener('mousemove', (e) => {
if (dragState) { // GC 2回目: まだ使用中で生存
updatePosition(dragState, e);
}
});
element.addEventListener('mouseup', () => {
dragState = null; // GC 3回目: ここで回収対象
});
3. 短期キャッシュ
const shortTermCache = new Map();
function expensiveCalculation(input) {
if (shortTermCache.has(input)) {
return shortTermCache.get(input); // GC 1-2回目: キャッシュヒット
}
const result = heavyComputation(input);
shortTermCache.set(input, result);
// 3秒後に削除
setTimeout(() => {
shortTermCache.delete(input); // GC 3-4回目: ここで回収
}, 3000);
return result;
}
4. UI状態の一時保持
let toastMessages = [];
function showToast(message) {
const toast = { // GC 1回目: 生存
id: Date.now(),
text: message,
createdAt: new Date()
};
toastMessages.push(toast); // GC 2回目: 配列で参照されて生存
renderToasts();
setTimeout(() => {
toastMessages = toastMessages.filter(t => t.id !== toast.id);
// GC 3回目: 配列から削除されて回収対象
}, 2000);
}
5. WebAPIのレスポンス待機
function apiCall() {
const requestData = { // GC 1回目: 生存
timestamp: Date.now(),
userId: getCurrentUser().id,
payload: buildPayload()
};
return fetch('/api/endpoint', {
method: 'POST',
body: JSON.stringify(requestData) // GC 2回目: fetch内部で参照
})
.then(response => response.json())
.then(data => {
// GC 3回目: requestDataはもう不要で回収
return processResponse(data);
});
}
なぜ即座に死なないのか?
タイミングの問題
- GCはいつ走るかわからない
- オブジェクトが不要になってもGCが走るまでは生存
- 複数の参照が段階的に解除される
参照の段階的解除
// 段階1: 作成
const data = createLargeObject();
// 段階2: 一時的に他の場所で参照
processingQueue.add(data); // まだ生きている
// 段階3: 処理完了、キューから削除
processingQueue.remove(data); // やっと回収可能
実際のイメージ
- 1回目GC: 「まだ使ってるかも?」→ 保留
- 2回目GC: 「まだ参照があるな」→ 保留
- 3回目GC: 「もう誰も参照してない」→ 回収
つまり、「短期間だが確実に必要な期間がある」オブジェクトが、この2〜3回で死ぬ典型例ということですね!
このスクラップは3ヶ月前にクローズされました