NettyのRecyclerによるオブジェクトプールの実装と内部構造

高頻度でオブジェクトが生成・破棄されるアプリケーションでは、ガベージコレクション(GC)の負荷が増大し、パフォーマンスに悪影響を及ぼす可能性があります。この問題を緩和するために、オブジェクトの再利用を可能にする「オブジェクトプール」技術が用いられます。Nettyが提供するRecyclerクラスは、スレッドローカルなオブジェクトプールとして設計されており、効率的なメモリ管理を実現します。

基本的な使用方法

まず、プール対象となるUserクラスを定義します。コンストラクタを非公開にして、ファクトリメソッドからインスタンスを取得するように制御します。

public class User {
    private static final Recycler<User> POOL = new Recycler<User>() {
        @Override
        protected User newObject(Handle<User> handle) {
            return new User(handle);
        }
    };

    public static User acquire() {
        return POOL.get();
    }

    private final Handle<User> handle;
    private Long id;
    private String name;

    private User(Handle<User> handle) {
        this.handle = handle;
        System.out.println(Thread.currentThread().getName() + " でインスタンス生成: " + this);
    }

    public void release() {
        System.out.println("解放: " + this);
        handle.recycle(this);
    }

    public void initialize(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // getter/setter...
}

テストコードでは、複数のスレッドがオブジェクトを取得・解放するシナリオを確認できます。

public class RecyclerDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            User u1 = User.acquire();
            User u2 = User.acquire();
            u2.release();
            User u3 = User.acquire(); // u2と同じインスタンス
            u1.release();
        });
        t1.start();
        t1.join();

        User u4 = User.acquire(); // メインスレッド用プール
        u4.release();
        User u5 = User.acquire(); // u4と同じインスタンス
    }
}

スレッドごとの独立性とクロススレッド回収

Recyclerは各スレッドに独立したプール(Stack)を持ちます。あるスレッドで生成されたオブジェクトは、別のスレッドから解放されても、元のスレッドのプールに戻されます。これは、DefaultHandleが生成時のStackへの参照を保持しているためです。

User user = User.acquire(); // メインスレッドで生成

new Thread(() -> {
    user.release(); // 別スレッドで解放
}).start();

Thread.sleep(100);

User reused = User.acquire(); // 同じインスタンスが再利用される

内部構造:StackとWeakOrderQueue

RecyclerのコアはStackクラスにあります。これはスレッドローカルなデータ構造で、以下の主要フィールドを持ちます。

  • elements: オブジェクトハンドルを格納する配列(スタック構造)
  • threadRef: 所属するスレッドへの弱参照
  • head: WeakOrderQueueの先頭への参照

DefaultHandleは、管理対象のオブジェクトとそのライフサイクルを制御するラッパーです。

static final class DefaultHandle<T> implements Handle<T> {
    private Stack<?> stack;
    private Object value;
    private int recycleId;

    DefaultHandle(Stack<?> stack) { this.stack = stack; }

    @Override
    public void recycle(Object o) {
        if (o != value) throw new IllegalArgumentException();
        stack.push(this); // 回収処理へ
    }
}

クロススレッド回収の仕組み

別スレッドからの回収要求は、WeakOrderQueueを通じて処理されます。これは、他のスレッドが回収したオブジェクトを一時的に保持するキュー構造です。

回収時の分岐処理:

void push(DefaultHandle<?> item) {
    if (threadRef.get() == Thread.currentThread()) {
        pushNow(item); // 同一スレッド → 直接push
    } else {
        pushLater(item); // 異なるスレッド → WeakOrderQueue経由
    }
}

pushLaterでは、現在のスレッドのFastThreadLocal<Map<Stack, WeakOrderQueue>>から、対象Stackに対応するWeakOrderQueueを取得し、そこにオブジェクトを追加します。

オブジェクトの取得と遅延回収

オブジェクト取得時(get())にローカルStackが空の場合、scavenge()メソッドが呼び出され、WeakOrderQueueからオブジェクトを回収してStackに移動させます。

transfer()メソッドにより、WeakOrderQueue内のLink(固定サイズのバッチ)からオブジェクトがStackにコピーされます。ただし、dropHandle()によって一定割合のオブジェクトは破棄され、プールの肥大化を防ぎます。

boolean dropHandle(DefaultHandle<?> handle) {
    if ((++handleRecycleCount & 7) != 0) {
        return true; // 8個中7個は破棄
    }
    return false;
}

この仕組みにより、頻繁なリサイズやメモリ消費を抑制しつつ、効率的なオブジェクト再利用を実現しています。

タグ: Netty Recycler オブジェクトプール ガベージコレクション スレッドローカル

6月17日 23:12 投稿