Netty series: the Reference count in the JVM is also found in netty

Posted by kooper on Mon, 14 Feb 2022 10:44:03 +0100

brief introduction

Why are there so many JAVA programmers in the world? One of the important reasons is that compared with C + +, JAVA does not need to consider the release of objects, and everything is completed by the garbage collector. In the modern programming world that advocates simplicity, there are fewer and fewer experts who can speak C + + and more and more programmers who can speak JAVA.

A very important concept in the JVM garbage collector is the Reference count, that is, the Reference count of the object, which is used to control whether the object is still referenced and whether it can be garbage collected.

Netty also runs in the JVM, so the object reference count in the JVM also applies to objects in netty. The object reference we mentioned here refers to some specific objects in netty. Whether these objects are still used can be determined by the reference count of objects. If they are no longer used, they (or their shared resources) can be returned to the object pool (or object allocator).

This is called netty's object reference counting technology. One of the most critical objects is ByteBuf.

ByteBuf and ReferenceCounted

The object reference count in netty is from 4 At the beginning of version x, ByteBuf is one of the final applications. It uses reference counting to improve allocation and release performance

Let's take a look at the definition of ByteBuf:

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf>

You can see that ByteBuf is an abstract class that implements the interface of ReferenceCounted.

ReferenceCounted is the basis of object reference in netty. It defines the following very important methods, as shown below:

int refCnt();

ReferenceCounted retain();

ReferenceCounted retain(int increment);

boolean release();

boolean release(int decrement);

Where refCnt returns the current number of references, retain is used to increase references, and release is used to release references.

Basic use of ByteBuf

In the case of just allocation, the number of references of ByteBuf is 1:

ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;

After calling his release method, refCnt becomes 0:

boolean destroyed = buf.release();
assert destroyed;
assert buf.refCnt() == 0;

When calling its retain method, refCnt will add one:

ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;
buf.retain();
assert buf.refCnt() == 2;

Note that if the refCnt of ByteBuf is already 0, it means that the ByteBuf is ready to be recycled. If the retain method is called again, IllegalReferenceCountException:refCnt: 0, increment: 1 will be thrown

So we must call the retain method before ByteBuf has been recovered.

Since the retain() method cannot be called when refCnt=0, can other methods be called?

Let's try calling the writeByte method:

        try {
            buf.writeByte(10);
        } catch (IllegalReferenceCountException e) {
            log.error(e.getMessage(),e);
        }

You can see that if refCnt=0, calling its writeByte method will throw an IllegalReferenceCountException exception.

In this way, as long as refCnt=0, this object has been recycled and can no longer be used.

ByteBuf recycling

Since refCnt is saved in ByteBuf, who is responsible for recycling ByteBuf?

netty's principle is that whoever consumes ByteBuf is responsible for the recycling of ByteBuf.

In actual work, ByteBuf will be transmitted in the channel. According to the principle of who consumes and who is responsible for destruction, the party receiving ByteBuf needs to recycle it if it consumes ByteBuf.

Recycling here refers to calling the release() method of ByteBuf.

Derivation method of ByteBuf

Bytebuf can derive many sub buffs from a parent buff. These sub buffs do not have their own reference count, and their reference count is shared with parent buffs. These methods to provide derived buffs are: bytebuff duplicate(), ByteBuf. Slice() and bytebuf order(ByteOrder).

buf = directBuffer();
        ByteBuf derived = buf.duplicate();
        assert buf.refCnt() == 1;
        assert derived.refCnt() == 1;

Because the derived byteBuf and parent buff share the reference count, if you want to pass the derived byteBuf to other processes for processing, you need to call the retain() method:

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();
}

Reference count in ChannelHandler

netty can be divided into InboundChannelHandler and OutboundChannelHandler according to whether to read or write messages, which are used to read and write messages respectively.

According to the principle of who consumes and who releases, for Inbound messages, after reading, you need to call the release method of ByteBuf:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    try {
        ...
    } finally {
        buf.release();
    }
}

However, if you only resend byteBuf to the channel for other steps, you do not need to release:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    ...
    ctx.fireChannelRead(buf);
}

Similarly, in Outbound, if it is just a simple retransmission, release is not required:

public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
    System.err.println("Writing: " + message);
    ctx.write(message, promise);
}

If the message is processed, release is required:

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);
    }
}

Memory leak

Because the reference count is maintained by netty itself, it needs to be release d manually in the program, which will lead to a problem of memory leakage. Because all references are controlled by the program itself, not by the JVM, some object reference counts may not be cleared due to personal reasons of the programmer.

In order to solve this problem, by default, netty will select 1% of the buffer allocations sample to detect whether they have memory leaks

If a leak occurs, you will get the following log:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

The above mentioned level for detecting memory leakage. netty provides four levels:

  • DISABLED --- disable leak detection
  • SIMPLE -- the default detection method, occupying 1% of the buff.
  • ADVANCED - also 1% buff for detection, but this option will show more leaked information.
  • PARANOID - detect all buff s.

Specific detection options are as follows:

java -Dio.netty.leakDetection.level=advanced ...

summary

If you master the reference count in netty, you will master the wealth password of netty!

Examples of this article can refer to: learn-netty4

This article has been included in http://www.flydean.com/43-netty-reference-cound/

The most popular interpretation, the most profound dry goods, the most concise tutorial, and many tips you don't know are waiting for you to find!

Welcome to my official account: "those things in procedure", understand technology, know you better!

Topics: Java jvm Netty