Closed7

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

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/ae9e6148264025


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

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

できたもの

https://github.com/lirlia/100day_challenge_backend/tree/main/day57_garbage_collector_system

riddle_tecriddle_tec

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

riddle_tecriddle_tec

なるほど!画面上の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ストラテジーの効果や動作タイミングを体験できるようになっています!

riddle_tecriddle_tec

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オブジェクトから参照される
  • 安定した参照関係を持つ

🎯 生き残りのコツ

  1. 長期間必要なデータは早めにグローバルスコープで参照
  2. キャッシュやシングルトンパターンの活用
  3. イベントハンドラーやコールバック関数の適切な管理
  4. クロージャーで状態を保持

つまり、「長く使われ続けるオブジェクト」だけがYoung Generationを卒業できるということですね!

riddle_tecriddle_tec

良い質問ですね!「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ヶ月前にクローズされました