リファレンスカウンティングオブジェクト
Nettyバージョン4以降、特定のオブジェクトのライフサイクルは参照カウントによって管理されるため、Nettyはそれら(または共有リソース)を、使用されなくなるとすぐにオブジェクトプール(またはオブジェクトアロケーター)に返すことができます。ガベージコレクションと参照キューは、到達不能性のリアルタイム保証を効率的に提供しませんが、リファレンスカウントはわずかな不便を犠牲にして代替メカニズムを提供します。
ByteBuf
は、参照カウントを利用して割り当てと解放のパフォーマンスを向上させる最も注目すべき型であり、このページでは、ByteBuf
を使用してNettyのリファレンスカウントの仕組みを説明します。
リファレンスカウントオブジェクトの初期参照カウントは1です。
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;
リファレンスカウントオブジェクトを解放すると、その参照カウントは1減ります。参照カウントが0になると、リファレンスカウントオブジェクトは解放されるか、元のオブジェクトプールに戻されます。
assert buf.refCnt() == 1;
// release() returns true only if the reference count becomes 0.
boolean destroyed = buf.release();
assert destroyed;
assert buf.refCnt() == 0;
参照カウントが0であるリファレンスカウントオブジェクトにアクセスしようとすると、IllegalReferenceCountException
がトリガーされます。
assert buf.refCnt() == 0;
try {
buf.writeLong(0xdeadbeef);
throw new Error("should not reach here");
} catch (IllegalReferenceCountExeception e) {
// Expected
}
参照カウントはまだ破棄されていない限り、retain()
操作によって増やすこともできます。
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;
buf.retain();
assert buf.refCnt() == 2;
boolean destroyed = buf.release();
assert !destroyed;
assert buf.refCnt() == 1;
経験則として、リファレンスカウントオブジェクトに最後にアクセスした当事者が、そのリファレンスカウントオブジェクトの破棄にも責任を持つべきです。具体的には
- [送信]コンポーネントがリファレンスカウントオブジェクトを別の[受信]コンポーネントに渡す必要がある場合、送信コンポーネントは通常、それを破棄する必要はなく、受信コンポーネントにその決定を委ねます。
- コンポーネントがリファレンスカウントオブジェクトを消費し、それ以上アクセスするものが何もない(つまり、別のコンポーネントに参照を渡さない)ことを知っている場合、そのコンポーネントはそれを破棄する必要があります。
簡単な例を次に示します。
public ByteBuf a(ByteBuf input) {
input.writeByte(42);
return input;
}
public ByteBuf b(ByteBuf input) {
try {
output = input.alloc().directBuffer(input.readableBytes() + 1);
output.writeBytes(input);
output.writeByte(42);
return output;
} finally {
input.release();
}
}
public void c(ByteBuf input) {
System.out.println(input);
input.release();
}
public void main() {
...
ByteBuf buf = ...;
// This will print buf to System.out and destroy it.
c(b(a(buf)));
assert buf.refCnt() == 0;
}
アクション | 誰が解放すべきか? | 誰が解放したか? |
---|---|---|
1. main() がbuf を作成する |
buf →main() |
|
2. main() がbuf と共にa() を呼び出す |
buf →a() |
|
3. a() は単にbuf を返す。 |
buf →main() |
|
4. main() がbuf と共にb() を呼び出す |
buf →b() |
|
5. b() はbuf のコピーを返す |
buf →b() 、copy →main() |
b() がbuf を解放する |
6. main() がcopy と共にc() を呼び出す |
copy →c() |
|
7. c() がcopy を消費する |
copy →c() |
c() がcopy を解放する |
ByteBuf.duplicate()
、ByteBuf.slice()
、ByteBuf.order(ByteOrder)
は、親バッファのメモリ領域を共有する派生バッファを作成します。派生バッファには独自の参照カウントがありませんが、親バッファの参照カウントを共有します。
ByteBuf parent = ctx.alloc().directBuffer();
ByteBuf derived = parent.duplicate();
// Creating a derived buffer does not increase the reference count.
assert parent.refCnt() == 1;
assert derived.refCnt() == 1;
対照的に、ByteBuf.copy()
とByteBuf.readBytes(int)
は派生バッファではありません。返されたByteBuf
は割り当てられ、解放する必要があります。
親バッファとその派生バッファは同じ参照カウントを共有し、派生バッファが作成されたときに参照カウントが増加しないことに注意してください。したがって、派生バッファをアプリケーションの他のコンポーネントに渡す場合は、最初にretain()
を呼び出す必要があります。
ByteBuf parent = ctx.alloc().directBuffer(512);
parent.writeBytes(...);
try {
while (parent.isReadable(16)) {
ByteBuf derived = parent.readSlice(16);
derived.retain();
process(derived);
}
} finally {
parent.release();
}
...
public void process(ByteBuf buf) {
...
buf.release();
}
場合によっては、ByteBuf
はDatagramPacket
、HttpContent
、WebSocketframe
などのバッファホルダーに含まれます。これらの型は、ByteBufHolder
と呼ばれる共通インターフェースを拡張します。
バッファホルダーは、派生バッファと同様に、それが含むバッファの参照カウントを共有します。
イベントループがデータを読み込んでByteBuf
に書き込み、それを含むchannelRead()
イベントをトリガーすると、対応するパイプライン内のChannelHandler
がバッファを解放する責任を負います。したがって、受信したデータを使用するハンドラーは、そのchannelRead()
ハンドラーメソッドでデータに対してrelease()
を呼び出す必要があります。
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
...
} finally {
buf.release();
}
}
このドキュメントの「誰が破棄するのか?」のセクションで説明したように、ハンドラーがバッファ(またはリファレンスカウントオブジェクト)を次のハンドラーに渡す場合は、解放する必要はありません。
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
...
ctx.fireChannelRead(buf);
}
ByteBuf
はNettyで唯一のリファレンスカウント型ではないことに注意してください。デコーダーによって生成されたメッセージを処理している場合、そのメッセージもリファレンスカウントされている可能性が非常に高いです。
// Assuming your handler is placed next to `HttpRequestDecoder`
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HttpRequest) {
HttpRequest req = (HttpRequest) msg;
...
}
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
try {
...
} finally {
content.release();
}
}
}
疑問がある場合、またはメッセージの解放を簡素化したい場合は、ReferenceCountUtil.release()
を使用できます。
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
...
} finally {
ReferenceCountUtil.release(msg);
}
}
あるいは、受信するすべてのメッセージに対してReferenceCountUtil.release(msg)
を呼び出すSimpleChannelHandler
を拡張することもできます。
インバウンドメッセージとは異なり、アウトバウンドメッセージはアプリケーションによって作成され、Nettyがそれらをワイヤーに書き込んだ後に解放する責任を負います。ただし、書き込み要求をインターセプトするハンドラーは、中間オブジェクト(例:エンコーダー)を適切に解放するようにする必要があります。
// Simple-pass through
public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
System.err.println("Writing: " + message);
ctx.write(message, promise);
}
// Transformation
public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
if (message instanceof HttpContent) {
// Transform HttpContent to ByteBuf.
HttpContent content = (HttpContent) message;
try {
ByteBuf transformed = ctx.alloc().buffer();
....
ctx.write(transformed, promise);
} finally {
content.release();
}
} else {
// Pass non-HttpContent through.
ctx.write(message, promise);
}
}
リファレンスカウントの欠点は、リファレンスカウントオブジェクトをリークしやすいことです。JVMはNettyが実装するリファレンスカウントを認識していないため、参照カウントがゼロでなくても、到達不能になると自動的にガベージコレクションします。オブジェクトは一度ガベージコレクションされると復活できず、したがって元のプールに戻ることができないため、メモリリークが発生します。
幸いなことに、リークの発見は困難ですが、Nettyはデフォルトでバッファ割り当ての約1%をサンプリングして、アプリケーションにリークがないかどうかを確認します。リークが発生した場合は、次のログメッセージが表示されます。
LEAK: ByteBuf.release() がガベージコレクションされる前に呼び出されませんでした。リークが発生した場所を特定するには、高度なリークレポートを有効にしてください。高度なリークレポートを有効にするには、JVMオプション「-Dio.netty.leakDetectionLevel=advanced」を指定するか、ResourceLeakDetector.setLevel()を呼び出します。
上記のJVMオプションを使用してアプリケーションを再起動すると、リークしたバッファにアクセスしたアプリケーションの最近の場所が表示されます。次の出力は、単体テスト(XmlFrameDecoderTest.testDecodeWithXml()
)からのリークを示しています。
Running io.netty.handler.codec.xml.XmlFrameDecoderTest
15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 1
#1:
io.netty.buffer.AdvancedLeakAwareByteBuf.toString(AdvancedLeakAwareByteBuf.java:697)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:157)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)
...
Created at:
io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155)
io.netty.buffer.UnpooledUnsafeDirectByteBuf.copy(UnpooledUnsafeDirectByteBuf.java:465)
io.netty.buffer.WrappedByteBuf.copy(WrappedByteBuf.java:697)
io.netty.buffer.AdvancedLeakAwareByteBuf.copy(AdvancedLeakAwareByteBuf.java:656)
io.netty.handler.codec.xml.XmlFrameDecoder.extractFrame(XmlFrameDecoder.java:198)
io.netty.handler.codec.xml.XmlFrameDecoder.decode(XmlFrameDecoder.java:174)
io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:227)
io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:140)
io.netty.channel.ChannelHandlerInvokerUtil.invokeChannelReadNow(ChannelHandlerInvokerUtil.java:74)
io.netty.channel.embedded.EmbeddedEventLoop.invokeChannelRead(EmbeddedEventLoop.java:142)
io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:317)
io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846)
io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:176)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:147)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)
...
Netty 5以降を使用している場合、リークしたバッファを最後に処理したハンドラーを特定するのに役立つ追加情報が提供されます。次の例は、リークしたバッファがEchoServerHandler#0
という名前のハンドラーによって処理され、その後ガベージコレクションされたことを示しています。これは、EchoServerHandler#0
がバッファの解放を忘れた可能性が高いことを意味します。
12:05:24.374 [nioEventLoop-1-1] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 2
#2:
Hint: 'EchoServerHandler#0' will handle the message from this point.
io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:329)
io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846)
io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:133)
io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)
io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)
io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)
java.lang.Thread.run(Thread.java:744)
#1:
io.netty.buffer.AdvancedLeakAwareByteBuf.writeBytes(AdvancedLeakAwareByteBuf.java:589)
io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:208)
io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:125)
io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)
io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)
io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)
java.lang.Thread.run(Thread.java:744)
Created at:
io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:146)
io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:107)
io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:123)
io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)
io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)
io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)
java.lang.Thread.run(Thread.java:744)
現在、リーク検出には4つのレベルがあります。
-
DISABLED
- リーク検出を完全に無効にします。推奨されません。 -
SIMPLE
- バッファの1%について、リークがあるかどうかを知らせます。デフォルト。 -
ADVANCED
- リークしたバッファにアクセスした場所をバッファの1%について知らせます。 -
PARANOID
- すべてのバッファについてADVANCED
と同じです。自動化されたテストフェーズに役立ちます。ビルド出力に「LEAK:
」が含まれている場合、ビルドを失敗させることができます。
リーク検出レベルをJVMオプション-Dio.netty.leakDetection.level
で指定できます。
java -Dio.netty.leakDetection.level=advanced ...
注記: このプロパティは以前はio.netty.leakDetectionLevel
と呼ばれていました。
PARANOID
リーク検出レベルとSIMPLE
レベルの両方で、単体テストと統合テストを実行します。- リークがないかどうかを確認するために、本番環境にデプロイする前に、
SIMPLE
レベルで合理的な時間、アプリケーションをカナリアテストします。 - リークがある場合、リークの原因に関するヒントを得るために、
ADVANCED
レベルで再度カナリアテストします。 - リークのあるアプリケーションを本番環境にデプロイしないでください。
単体テストでバッファまたはメッセージの解放を忘れることは非常に簡単です。リーク警告が生成されますが、必ずしもアプリケーションにリークがあることを意味するわけではありません。すべてのバッファを解放するために単体テストをtry-finally
ブロックでラップする代わりに、ReferenceCountUtil.releaseLater()
ユーティリティメソッドを使用できます。
import static io.netty.util.ReferenceCountUtil.*;
@Test
public void testSomething() throws Exception {
// ReferenceCountUtil.releaseLater() will keep the reference of buf,
// and then release it when the test thread is terminated.
ByteBuf buf = releaseLater(Unpooled.directBuffer(512));
...
}
外部リンク
JVMのGCがまだ存在する場合、Netty ByteBufのリファレンスカウントを手動で処理する必要があるのはなぜですか?