Netty 的 ByteBuf 是如何支持 堆内存非池化 实现的

Netty的ByteBuf是如何支持堆内存非池化实现的

ByteBuffer 从实现方式上分成
HeapByteBuffer 和 DirectByteBuffer 两种内存实现方式,
HeapByteBuffer 底层使用 byte 数组存储数据,
DirectByteBuffer 底层使用 unsafe 操作直接内存
Java 原生的 Buffer 有三种不同的分类方式:按数据类型、按内存实现方式、按读写方式,可以分成不同的 Buffer。

那么,今天,我想问:
1 Netty 中的 ByteBuf 有没有兄弟类呢,即 CharBuf 等?
2 Netty 中的 ByteBuf 有哪些分类方式?
3 Netty 中的 ByteBuf 各种实现方式的底层原理是什么?

在这里插入图片描述
从继承体系上看,ByteBuf 有八个主要实现类,这八个实现类又可以按三个维度来划分:

1 内存实现方式:Heap 和 Direct;
2 池化与否:Pooled 和 Unpooled;
3 Unsafe 与否:Unsafe 和 Safe(类名不带 Unsafe 的即为 Safe);

ByteBuf 并没有类似于 CharBuf、IntBuf 这样的兄弟类。
经过上一节的学习,内存实现方式这个维度我们都比较熟悉了,另外两个维度对于我们可能就比较懵了:

1 池化,这里的池跟线程池、数据库连接池是一样的概念吗?ByteBuf 如何池化?
2 Unsafe,这里的 Unsafe 跟 Java 原生的 Unsafe 有关系吗?上一节不是说 DirectByteBuffer 底层就是通过 Unsafe 操作直接内存实现的吗?这里的 Unsafe 又是几个意思?


// 使用池化技术
public class PooledByteBufAllocator extends AbstractByteBufAllocator implements ByteBufAllocatorMetricProvider {
    public static final PooledByteBufAllocator DEFAULT =
            new PooledByteBufAllocator(PlatformDependent.directBufferPreferred());
    // 省略其它代码
}

// 不使用池化技术
public final class UnpooledByteBufAllocator extends AbstractByteBufAllocator implements ByteBufAllocatorMetricProvider {
    public static final UnpooledByteBufAllocator DEFAULT =
            new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
    // 省略其它代码
}

// 更偏向于使用堆内存
public final class PreferHeapByteBufAllocator implements ByteBufAllocator {
    private final ByteBufAllocator allocator;
    // 省略其它代码
}
// 更偏向于使用直接内存
public final class PreferredDirectByteBufAllocator implements ByteBufAllocator {
    private ByteBufAllocator allocator;
    // 省略其它代码
}

好了,我们再来看看 ByteBufAllocator 这个接口的结构。

public interface ByteBufAllocator {

    // 默认的分配器,除非显式地配成unpooled,否则使用pooled
    ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
    
    // 创建一个ByteBuf,看具体的实现方式决定创建的是direct的还是heap的
    ByteBuf buffer();
    ByteBuf buffer(int initialCapacity);
    ByteBuf buffer(int initialCapacity, int maxCapacity);

    // 创建一个heap类型的ByteBuf
    ByteBuf heapBuffer();
    ByteBuf heapBuffer(int initialCapacity);
    ByteBuf heapBuffer(int initialCapacity, int maxCapacity);

    // 创建一个direct的ByteBuf
    ByteBuf directBuffer();
    ByteBuf directBuffer(int initialCapacity);
    ByteBuf directBuffer(int initialCapacity, int maxCapacity);
    
	// 省略其它方法
 }

有这几个方法,完全够我们使用了,通过观察可以发现,每种方法都提供了三种重载方式,且参数的变化是跟容量相关的,所以,我们是否可以大胆猜测:其实,ByteBuf 是支持扩容的呢?
支不支持扩容呢?下面进入我们的微观分析阶段。

微观分析 ByteBuf
通过宏观分析,我们知道 ByteBuf 有 8 个主要实现类,我们又知道,可以通过 ByteBufAllocator 来创建不同类型的 ByteBuf,但是,并没有看到跟 Unsafe 相关的代码,这是一个问题,等待我们去挖掘。
所以,我们先从简单的 Unpooled 和 Heap 入手,这样怎么写调试用例呢?其实很简单,请看:

public class ByteBufTest {
    public static void main(String[] args) {
    
        // 1. 参数是preferDirect,即是否偏向于使用直接内存
        UnpooledByteBufAllocator allocator = new UnpooledByteBufAllocator(false);
        
        // 2. 创建一个非池化基于堆内存的ByteBuf
        ByteBuf byteBuf = allocator.heapBuffer();
        
        // 3. 写入数据
        byteBuf.writeInt(1);
        byteBuf.writeInt(2);
        byteBuf.writeInt(3);

        // 4. 读取数据
        System.out.println(byteBuf.readInt());
        System.out.println(byteBuf.readInt());
        System.out.println(byteBuf.readInt());
    }
}

这个调试用例,我将分成 4 个部分来进行源码级别的剖析:

1 创建 ByteBufAllocator 的过程;
2 创建 heapByteBuf 的过程;
3 写入数据的过程;
4 读取数据的过程;

首先,让我们看看创建 ByteBufAllocator 的过程:

// 参数1preferDirect表示是否偏向于使用直接内存
// 这里我们传的是false
public UnpooledByteBufAllocator(boolean preferDirect) {
    this(preferDirect, false);
}
// 参数2表示是否禁用内存泄漏检测,先跳过
public UnpooledByteBufAllocator(boolean preferDirect, boolean disableLeakDetector) {
    this(preferDirect, disableLeakDetector, PlatformDependent.useDirectBufferNoCleaner());
}
// 参数3表示是否尝试没有Cleaner的构造方法,这个是什么意思呢?
public UnpooledByteBufAllocator(boolean preferDirect, boolean disableLeakDetector, boolean tryNoCleaner) {
    super(preferDirect);
    this.disableLeakDetector = disableLeakDetector;
    noCleaner = tryNoCleaner && PlatformDependent.hasUnsafe()
        && PlatformDependent.hasDirectBufferNoCleanerConstructor();
}
// 调用父类的构造方法
protected AbstractByteBufAllocator(boolean preferDirect) {
    // 如果偏向于直接内存且平台有Unsafe,则默认使用直接内存
    // 这里的Unsafe是Java原生的Unsafe吗?
    directByDefault = preferDirect && PlatformDependent.hasUnsafe();
    emptyBuf = new EmptyByteBuf(this);
}

首先,会检测我们是否显式地关闭了 Unsafe,即通过参数 io.netty.noUnsafe 控制的,其实,Netty 中很多场景都是可以通过参数显式地控制的,但是,一般我们也没有必要去修改默认配置,因为 Netty 给我们的默认配置已经足够好了。

然后,会尝试反射访问 Unsafe 中的属性 theUnsafe 和方法 copyMemory,其中,theUnsafe 是我们获得 Unsafe 实例的唯一方法,因为这个类是 Java 核心类,有非常严格的权限控制,我们只能通过这种方式获得其实例。

最后,会反射获取 DirectByteBuffer 中的 address 属性,这个 address 是定义在其父类 Buffer 中的。

如果上面三步都成功了,才能宣判我们可以正确地使用 Unsafe 了,当然了,这个 Unsafe 就是 Java 原生的那个 Unsafe。

另外,前面看到的 PlatformDependent.hasDirectBufferNoCleanerConstructor() 最终也是在这个静态块中判断的,原理都差不多,无非是通过反射判断某个方法或者属性存不存在,有兴趣的同学可以研究下。

InstrumentedUnpooledUnsafeHeapByteBuf(UnpooledByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
    // 调用父类的构造方法
    super(alloc, initialCapacity, maxCapacity);
}
public UnpooledUnsafeHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
    // 调用父类的构造方法
    super(alloc, initialCapacity, maxCapacity);
}
public UnpooledHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
    // 这个父类构造方法里没什么重要的东西,不断续往里跟了
    super(maxCapacity);

    // 检查两个容量
    if (initialCapacity > maxCapacity) {
        throw new IllegalArgumentException(String.format(
            "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
    }

    this.alloc = checkNotNull(alloc, "alloc");
    // 调用allocateArray创建了一个byte数组,并保存起来
    setArray(allocateArray(initialCapacity));
    // 初始化readIndex和writeIndex为0
    setIndex(0, 0);
}
// InstrumentedUnpooledUnsafeHeapByteBuf#allocateArray
// 这个方法就是做了增强的方法
@Override
protected byte[] allocateArray(int initialCapacity) {
    byte[] bytes = super.allocateArray(initialCapacity);
    // 增强的地方
    ((UnpooledByteBufAllocator) alloc()).incrementHeap(bytes.length);
    return bytes;
}

到这里就比较清楚了,UnpooledUnsafeHeapByteBuf 底层还是使用的 Java 原生的 byte 数组来实现的,至于这里增强的方法,其实是加入了监控,打开 InstrumentedUnpooledUnsafeHeapByteBuf 这个类,你会发现,它里面还有另一个方法叫作 freeArray (),它做的增强即:当创建 byte 数组的时候记录下来分配的堆内存大小,当释放 byte 数组的时候将其占用的堆内存大小相应减少。这样就起来了监控的目的,即 Netty 可以监控进程中所有的 UnpooledUnsafeHeapByteBuf 到底占用了多少堆内存,方便出问题时进行排查,当然,也可以用于提前发现内存泄漏等问题。

OK,经过前面的过程,我们终于创建了一个 UnpooledUnsafeHeapByteBuf 对象,接下来我们再来看看写入数据的过程吧,即 byteBuf.writeInt(1); 这行代码,继续跟踪进去:

@Override
public ByteBuf writeInt(int value) {
    // 一个int等于4个字节
    // 检查是否可写,里面会做扩容相关的操作,
    // 最终会调用allocateArray()分配一个新的数组,并把旧数组的数据拷贝到新数组
    // 且调用freeArray()把旧数组释放,当然,此时也会改变上面提到的监控的数值
    ensureWritable0(4);
    // 在写索引的位置开始写入值
    _setInt(writerIndex, value);
    // 写索引加4
    writerIndex += 4;
    return this;
}
@Override
protected void _setInt(int index, int value) {
    // 调用UnsafeByteBufUtil工具类修改array数组index位置的值
    UnsafeByteBufUtil.setInt(array, index, value);
}
static void setInt(byte[] array, int index, int value) {
    // 我的电脑UNALIGNED=true
    if (UNALIGNED) {
        // 又到了PlatformDependent这个类中
        PlatformDependent.putInt(array, index, BIG_ENDIAN_NATIVE_ORDER ? value : Integer.reverseBytes(value));
    } else {
        PlatformDependent.putByte(array, index, (byte) (value >>> 24));
        PlatformDependent.putByte(array, index + 1, (byte) (value >>> 16));
        PlatformDependent.putByte(array, index + 2, (byte) (value >>> 8));
        PlatformDependent.putByte(array, index + 3, (byte) value);
    }
}
public static void putInt(byte[] data, int index, int value) {
    // 继续到PlatformDependent0这个类中
    PlatformDependent0.putInt(data, index, value);
}
static void putInt(byte[] data, int index, int value) {
    // 调用Unsafe的putInt()方法直接修改对象的属性
    // 数组本身就是一种特殊的对象,它也有对象头等属性
    // 所以,需要一个偏移量,BYTE_ARRAY_BASE_OFFSET=16
    UNSAFE.putInt(data, BYTE_ARRAY_BASE_OFFSET + index, value);
}
// native方法
// 这个putInt()跟上一节修改直接内存的putInt()不是同一个方法
// 上一节的putInt(long var1, int var3)第一个参数是内存地址
// 本节的putInt()第一个参数是对象,第二参数是在对象中的偏移量
public native void putInt(Object var1, long var2, int var4);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值