驽马十驾 驽马十驾

驽马十驾,功在不舍

目录
Netty默认的Bytebuf是堆内还是堆外?池化or非池化?
/  

Netty默认的Bytebuf是堆内还是堆外?池化or非池化?

开篇

NettyByteBuf有从不同角度有如下2个分类,4种组合!

  • 堆外内存和堆内内存
  • 池化和非池化

我们在利用Netty做底层通信框架的时候,会默认给我们的到底是哪一种组合了?

分析

池化分析

Netty的Boostrap启动类按照标准模板,通常会添加这个配置option(ChannelOption.ALLOCATOR, ByteBufAllocator.DEFAULT)

val serverBootstrap = ServerBootstrap().group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel::class.java) //配置客户端的channel类型
                    .option(ChannelOption.ALLOCATOR, ByteBufAllocator.DEFAULT)
                    //.....

那么通过默认的ByteBufAllocator.DEFAULT分配出来的是哪一种组合了?我们来通过代码分析。

首先看看这个ByteBufAllocator.DEFAULT创建了一个什么类

public interface ByteBufAllocator {
    ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
}

继续调用下去,下述代码省略了一些无关的细节!

public final class ByteBufUtil {

    static final ByteBufAllocator DEFAULT_ALLOCATOR;

    static {
      	//核心语句
        String allocType = SystemPropertyUtil.get(
                "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
        allocType = allocType.toLowerCase(Locale.US).trim();
        ByteBufAllocator alloc;
      
        if ("unpooled".equals(allocType)) {
            alloc = UnpooledByteBufAllocator.DEFAULT;
        } else if ("pooled".equals(allocType)) {
            alloc = PooledByteBufAllocator.DEFAULT;
        } else {
            alloc = PooledByteBufAllocator.DEFAULT;
        }
				//这里赋值了噢...
        DEFAULT_ALLOCATOR = alloc;
    }

上述代码最为核心的一句是:

String allocType = SystemPropertyUtil.get(
                "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled")

其表达的意思是

  1. 系统配置(通常是启动的命令行给的)中读取io.netty.allocator.type,如果能够读取到,那么按照配置设置。但是这个参数我们通常是没有设置的,所以会进行第二步判定
  2. 当读取不到的情况下,又进行了一个判定:PlatformDependent.isAndroid(),当Netty运行在Android的时候是unpooled,否则是pooled

后面的代码就是根据pooledunpooled来进行对象初始化了。

综上,我们大多数时候启动参数都是没有加对应配置的,且运行在非安卓的系统,所以是pooledByteBuf

堆外/堆外

接下来还需要确定默认情况下分配的ByteBuf是分配到堆内还是堆外的,从PooledByteBufAllocator.DEFAULT;点进去可以看到这个代码

public static final PooledByteBufAllocator DEFAULT =
            new PooledByteBufAllocator(PlatformDependent.directBufferPreferred());

这个构造函数的参数directBufferPreferred的中文意思不就是偏向堆外内存吗?不过按照传统这个应该是一个boolean的变量,猜测了这些,我们继续看经过省略的源码PlatformDependent

public final class PlatformDependent {
	private static final boolean DIRECT_BUFFER_PREFERRED;

	static {
		if (!isAndroid()) {
			if (javaVersion() >= 9) {
				CLEANER = CleanerJava9.isSupported() ? new CleanerJava9() : NOOP;
			} else {
				CLEANER = CleanerJava6.isSupported() ? new CleanerJava6() : NOOP;
			}
		} else {
			CLEANER = NOOP;
		}
		DIRECT_BUFFER_PREFERRED = CLEANER != NOOP && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);
	}

核心的参数DIRECT_BUFFER_PREFERRED要想为true必须经过2步:

  1. 通过判定获取CLEANER,这个判定牵扯到了另外的判定,这里就不贴代码了,意思应该就是有没有堆外内存的清理办法,Java7+应该都有的
  2. 然后判定系统参数io.netty.noPreferDirect是不是为false

通常Java后端的系统这2个条件都是满足的,所以恭喜:DIRECT_BUFFER_PREFERREDtrue,堆外内存。

优劣

通过上面分析,我们知道了Netty默认的ByteBuf是堆外的池化模式,那么池化与否堆外与否各自的优劣在哪里!?

通常池化意味者可以复用资源,减少系统资源的开销

堆外内存可以使用更多可用的空间,毕竟Java虚拟机的大小一般都要小于系统的大小。但是江湖传言堆外内存的开辟比堆内内存的开辟慢,这一点我不太能理解,这里记录下。

以下是我在发文后的第二天从java.nio.ByteBuffer看到的文档注释!

A direct byte buffer may be created by invoking the {@link #allocateDirect(int) allocateDirect} factory method of this class. The buffers returned by this method typically have somewhat higher allocation and deallocation costs than non-direct buffers.

上文的意思翻译下就是

堆外内存的创建可以通过调用allocateDirect这个工厂方法来创建。通过这个方法创建的buffer通常来说会比非堆外内存有更高的创建和释放成本。

这里我还是纳闷啊!**不是说C的效率要高于Java吗?为什么堆外内存的创建和释放会比堆内内存的成本更高了?**有知道的大佬吗?

结语

本篇文章主要通过启动参数的配置,进行了源码分析,确认了Netty的ByteBuf使用的是堆外的池外模式

骐骥一跃,不能十步。驽马十驾,功在不舍。