Skip to content

メモリ管理

YanmaJS はマーク&スイープ方式のガベージコレクタ(GC)を内蔵しており、通常は特別な操作をしなくても自動的にメモリが管理されます。このページでは、メモリ上限の設定、GC の手動制御、そして GC を跨いで Value を生存させるための仕組み(ハンドルスコープ・永続ハンドル・finalizer)について説明します。

GC の概要

make* 系の関数(makeObjectmakeArraymakeString など)で作成されたオブジェクトは、ヒープ上に確保され、GC の管理下に置かれます。GC は到達可能性(グローバルオブジェクト・実行中のスタック・登録済みのハンドルなどから辿れるかどうか)を基準に、不要になったオブジェクトを自動的に回収します。

デフォルト(GCMode.auto)では、オブジェクト数が一定の閾値を超えるたびに自動的に GC が実行されます。

メモリ上限を設定する

信頼できない JavaScript コードをサンドボックス実行する場合など、メモリ使用量に上限を設けたいケースでは Context.initWithMemoryLimit を使います。

const ctx = try Context.initWithMemoryLimit(allocator, 1 * 1024 * 1024); // 1MiB
defer ctx.deinit();

const result = ctx.eval(large_script) catch |err| {
    if (err == error.OutOfMemory) {
        // メモリ上限に達した
    }
    return err;
};
  • Context.initWithMemoryLimit(backing_allocator: std.mem.Allocator, max_bytes: usize) !*Context — 内部でアロケーション量を追跡するアロケータでラップした Context を作成します。上限を超えるアロケーションは error.OutOfMemory になります。
  • ctx.getMemoryUsage() usize — 現在の使用量(バイト)。init(上限なし)で作成した場合は常に 0
  • ctx.getMemoryLimit() ?usize — 設定された上限。init で作成した場合は null

手動で GC を実行する

ctx.collectGarbage();
  • ctx.collectGarbage() void — GC モードに関わらず、その場でガベージコレクションを 1 回実行します。

GC モード

pub const GCMode = enum { auto, manual };

ctx.setGCMode(.manual);
  • ctx.setGCMode(mode: GCMode) void
  • .auto(デフォルト) — オブジェクト数が閾値を超えるたびに自動的に GC が走ります。
  • .manual — 自動 GC を無効化します。collectGarbage() を明示的に呼ばない限り回収されません。オブジェクトの生成タイミングを厳密に制御したい場合や、GC の一時停止(下記)よりも明示的に扱いたい場合に使います。

GC を一時的に抑制する: suppressGC / resumeGC

クリティカルセクション(GC が走ると困る処理)の間だけ自動 GC を止めたい場合は suppressGC/resumeGC を使います。

ctx.suppressGC();
defer ctx.resumeGC();

// この区間では自動 GC の閾値チェックが無効化される
// (collectGarbage() を明示的に呼べば GC 自体は実行できる)
  • ctx.suppressGC() void — 現在の閾値を退避し、実質的に自動 GC が発生しないようにします。
  • ctx.resumeGC() void — 退避しておいた閾値を復元し、自動 GC を再開します。

suppressGC は「自動 GC のトリガーを抑制する」ものであり、collectGarbage() を明示的に呼び出した場合はその抑制中でも GC が実行される点に注意してください。

GC の統計情報

const stats = ctx.getGCStats();
// stats.object_count      現在生存しているオブジェクト数
// stats.threshold         次の自動 GC が発生する閾値
// stats.collection_count  これまでに実行された GC の回数
// stats.last_freed_count  直近の GC で解放されたオブジェクト数
  • ctx.getGCStats() GCStats
pub const GCStats = struct {
    object_count: usize,
    threshold: usize,
    collection_count: usize,
    last_freed_count: usize,
};

GC のチューニング

初期閾値や閾値の増加率を調整したい場合は setGCConfig を使います。

ctx.setGCConfig(.{
    .initial_threshold = 64,
    .growth_factor = 3,
    .min_threshold = 64,
});
pub const GCConfig = struct {
    initial_threshold: usize = 256,
    growth_factor: usize = 2,
    min_threshold: usize = 256,
};
  • initial_threshold — 設定直後の GC 閾値(この呼び出しの時点で ctx.getGCStats().threshold にもすぐ反映されます)。
  • growth_factor — GC 実行後、生存オブジェクト数を基準に次の閾値をどれだけ引き上げるかの倍率。
  • min_threshold — 閾値の下限。

テスト・デバッグ用途で GC の発生頻度を極端に上げたい場合は、initial_threshold/min_threshold を小さい値にすると、少ないオブジェクト生成でも GC が頻繁に走るようになります。

GC を跨いで Value を生存させる

make* 系関数が返す Value は、作成した時点ではまだどこからも「ルート」から辿れない状態です。setGlobal/setProperty/setIndex などで実際に JavaScript から到達可能なツリーにアタッチするまでの間に GC が走ると、回収されてしまう可能性があります。これを防ぐための仕組みが 2 つあります。

ハンドルスコープ: handleScope

一時的な操作(複数の Value を組み立てている最中など)の間だけ、まとめて GC から保護したい場合は HandleScope を使います。

var scope = ctx.handleScope();
defer scope.close();

const obj = try ctx.eval("({value: 42})");
scope.pin(obj);

ctx.collectGarbage(); // obj は保護されているので回収されない
  • ctx.handleScope() HandleScope — スコープを開始します。
  • scope.pin(val: Value) void — 値をスコープに登録して保護します(オブジェクト以外の値は無視されます)。
  • scope.close() void — スコープを閉じ、登録されていた保護をまとめて解除します。

defer scope.close() のパターンで、関数を抜ける際に必ず解除されるようにするのが基本形です。

永続ハンドル: protect / unprotect

関数のスコープを超えて長期間 Value を保持したい場合(ホスト側の状態に格納する、コールバックの完了まで持ち越す、など)は PersistentHandle を使います。

const handle = ctx.protect(obj);
defer ctx.unprotect(handle);

ctx.collectGarbage(); // obj は保護されているので回収されない
  • ctx.protect(val: Value) PersistentHandle — 値を GC のルートとして登録します。
  • ctx.unprotect(handle: PersistentHandle) void — 登録を解除します。

重要: ホスト側のデータ構造(構造体のフィールドや配列など)に Value を保存して後で使う場合は、必ず protect して得た PersistentHandle の寿命をその Value の寿命と合わせて管理してください。protect せずに生の Value だけを保存すると、GC のタイミングでオブジェクトが回収され、無効なポインタを参照してしまいます。

Promisehandle(PromiseHandle)は、内部的にこの protect/unprotect の仕組みを使って実装されています(詳しくは Promise と非同期処理 を参照)。

finalizer によるネイティブリソースの解放

makeHostObject/setHostData で JavaScript オブジェクトにネイティブリソースを紐付けた場合、そのオブジェクトが GC によって回収されるタイミングで finalizer が呼ばれます。

const obj = try ctx.makeHostObject(@ptrCast(resource), FinalizerImpl.cleanup);

finalizer の呼び出しタイミングは GC の実行タイミング(自動 GC または collectGarbage() の呼び出し)に依存するため、決定的なタイミングでリソースを解放したい場合(ファイルディスクリプタのクローズなど)は、finalizer だけに頼らずホスト側で明示的な解放処理も用意することを検討してください。オブジェクト・host object の詳細は オブジェクトと配列 を参照してください。

次のステップ