“This is the 16th day of my participation in the November Gwen Challenge. See details of the event: The Last Gwen Challenge 2021”.

Both NIO’s ByteBuffer and Netty’s ByteBuf have ways of using direct memory.

In Netty, when we run out of direct memory, we need to release it manually instead of waiting for GC to reclaim it to reduce the risk of running out of memory.

Type of ByteBuf

There are many kinds of them. According to the pooling mechanism mentioned above, we can divide them into two main categories, which are also divided into heap memory and direct memory:

  • UnpooledHeapByteBuf: Unpooled heap memory ByteBuf, which is managed by JVM memory and can wait for GC collection.
  • UnpooledDirectByteBuf: Unpooled direct memory ByteBuf, which is not managed by the JVM. Although it can be reclaimed by GC, it is not timely and may run out of memory and need to be reclaimed manually.
  • PooledByteBuf: PooledByteBuf, which has a more complex recycling paradigm, will be looked at in implementation details through source code analysis later.
    • PooledHeapByteBuf: pooled heap memory ByteBuf
    • PooledDirectByteBuf: pooled direct memory ByteBuf

Principle of direct memory reclamation

In previous articles, we talked briefly about the structure of ByteBuf:

public abstract class ByteBuf implements ReferenceCounted

As shown above, it implements the ReferenceCounted interface, which translates to “reference counting.”

Believe that studied the JVM GC students should know something about “reference counting method”, when an object has a reference, we have to counter plus 1, on the other hand is minus 1, but the reference counting method cannot deal with ring, so put forward the “root of algorithm”, behind the simple way, need to know the details of the friends can see my project [JVM].

The reference count here, used for ByteBuf’s direct memory reclamation, takes a look at its main methods:

Public interface ReferenceCounted {/** * return the current object reference count */ int refCnt(); /** * increment the reference count by 1 */ ReferenceCounted retain(); /** * Increment increment from increment */ ReferenceCounted retain(int increment); /** * Reduce the reference count by 1 and unallocate the object when the reference count reaches 0 */ Boolean release(); /** * Reduces the reference count by the specified decrement and unallocates the object if the reference count reaches 0. */ boolean release(int decrement); }Copy the code

All bytebufs implement this interface, and when a new ReferenceCounted is instantiated, it starts with a reference count of 1. Retain () increases the reference count, while release() decreases it. If the reference count is reduced to zero, the object is freed, and accessing the freed object usually results in an access conflict.

Try it out briefly with the following code:

public static void main(String[] args) { ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(); // Prints the current reference count system.out.println (" initialized reference count "+ bytebuf.refcnt ()); Bytebuf.release (); // Prints the current reference count system.out.println (" Freed reference count "+ bytebuf.refcnt ()); // Call byteBuf try {bytebuf.writeint (888); } catch (Exception e) {system.out.println (" call Exception: "+ e); } // Add reference count try {bytebuf.retain (); } catch (Exception e) {system.out.println (" add reference count Exception: "+ e); } / / redistribution byteBuf = ByteBufAllocator. DEFAULT. The buffer (); // Call byteBuf bytebuf.writeint (888); System.out.println(" reassigned reference count "+ bytebuf.refcnt ()); }Copy the code

Results:

Initialize the reference count of 1 released after reference counting zero release after invoke exception: io.net ty. Util. IllegalReferenceCountException: refCnt: 0 is released to increase the reference count abnormality: io.net ty. Util. IllegalReferenceCountException: refCnt: 0, increment: 1 after the redistribution of reference counting 1Copy the code

When the reference count goes to zero, the entire memory is freed. Using it again will throw an exception, and trying to increase the reference count again will throw an exception, which can only be reallocated.

Three, the use of memory release

3.1 Manual Release

Now that we have a brief introduction to memory freeing, how can we use it? Could it be called in finally, as we are used to in Java code?

try {

} finally {
    byteBuf.release();
}
Copy the code

You can’t just jump to conclusions.

As we mentioned in the introduction, there is a chance that memory will run out, and even if it doesn’t, it will waste memory.

In the previous article, we studied Pipeline and Handler. Normally we pass one byteBuf to another channelHandler for processing, there is a transitivity. There are two situations:

  • Suppose there are five ChannelHandlers. In the second one, byteBuf is converted to a Java object and passed to the third channelHandler. ByteBuf is no longer useful, so it should be released.
  • ByteBuf is passed until the last channelHandler releases it.

In conclusion, whoever runs out of them will be responsible for releasing them.

Suggestion: If you are sure that the BUF is running out at the last minute and you are not sure how many reference counts there are, use the following two ways to release:

  • Loop through release() until it returns true.
  • Get the current reference count by refCnt() and then release(int refCnt).

3.2 Tail and head are automatically released

Remember from the previous section on Pipeline and Handler, we mentioned the concept of head and tail. In addition to our own Handler, we will have a head and tail Handler by default.

In both processors, there is also the ability to automatically reclaim memory, but only if we pass byteBuf to head or tail. If we convert the type halfway, we still need to free resources ourselves.

We’ve also looked at the inbound and outbound handlers, where the inbound handler passes content using the channelRead() method and the outbound handler passes parameters using the write method, which will serve as a marker for our tracking code.

Let’s briefly trace the source code to see how to achieve memory release. We track the pipeline addLast method, tracking the AbstractChannelHandlerContext this abstract class, it has two implementation class:

That corresponds to our head and tail processors.

3.2.1 TailContext

We’ll start with the Tail processor, which implements ChannelInboundHandler, the inbound processor, for inbound work.

final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler
Copy the code

Find the channelRead method:

       public void channelRead(ChannelHandlerContext ctx, Object msg) {
            DefaultChannelPipeline.this.onUnhandledInboundMessage(ctx, msg);
        }
Copy the code

Continue to follow onUnhandledInboundMessage

protected void onUnhandledInboundMessage(Object msg) { try { logger.debug("Discarded inbound message {} that reached at the tail of the pipeline. Please check your pipeline configuration.", msg); } finally { ReferenceCountUtil.release(msg); }}Copy the code

Finding the reference counting utility class, call the release method:

ReferenceCountUtil.release(msg);

Whether MSG implements ReferenceCounted? If yes, return false otherwise.

    public static boolean release(Object msg) {
        return msg instanceof ReferenceCounted ? ((ReferenceCounted)msg).release() : false;
    }
Copy the code

3.2.1 HeadContext

If you look at HeadContext, you implement ChannelOutboundHandler, the outbound handler, to do the outbound work.

final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler
Copy the code

Find its write method:

        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
            this.unsafe.write(msg, promise);
        }
Copy the code

Continue tracking write:

public final void write(Object msg, ChannelPromise promise) { this.assertEventLoop(); ChannelOutboundBuffer outboundBuffer = this.outboundBuffer; if (outboundBuffer == null) { this.safeSetFailure(promise, this.newClosedChannelException(AbstractChannel.this.initialCloseCause)); ReferenceCountUtil.release(msg); } else { int size; try { msg = AbstractChannel.this.filterOutboundMessage(msg); size = AbstractChannel.this.pipeline.estimatorHandle().size(msg); if (size < 0) { size = 0; } } catch (Throwable var6) { this.safeSetFailure(promise, var6); ReferenceCountUtil.release(msg); return; } outboundBuffer.addMessage(msg, size, promise); }}Copy the code

In the code above, it is still found

ReferenceCountUtil.release(msg)

Other code will not be explained in this article.

Both head and tail need to pass the BUF in order to release.


This article covers all of these for now, and I will continue to do so. Please give me a thumbs up if it helps.