文章

ByteBuf——Netty定义的NIO ByteBuffer升级组件

ByteBuf——Netty定义的NIO ByteBuffer升级组件

引言

NIOBuffer本质是一个内存块,NIOBuffer类是一个位于java.nio包的抽象类,它有8种缓冲区子类,分别是ByteBufferCharBufferDoubleBufferFloatBufferIntBufferLongBufferShortBufferMapperByteBuffer,其中使用最多的就是ByteBuffer二进制字节缓冲区类型。

这些子类会拥有一块内存,作为数据的读写缓冲区,比如ByteBuffer就有一个byte[]类型的数组成员作为自己的读写缓冲区。

NIOByteBuffer局限性,如下,

  • ByteBuffer长度固定,一旦完成分配,它的容量不能动态扩展和收缩,当需要编码的对象大于ByteBuffer的容量时,会发生索引越界异常;
  • ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;
  • ByteBufferAPI功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。

由于上面的局限,才有了Netty自己封装的ByteBuf。为了更高效地操作内存缓冲区,Netty提供了ByteBuf缓冲区来替代NIOByteBuffer

ByteBuf是什么

ByteBuf是一个字节容器,内部是一个字节数组,从逻辑上来分,字节容器内部分为四个部分,

  • 第一部分是已用字节,表示已经使用完的废弃无效字节;
  • 第二部分是可读字节,表示保存的有效数据,从ByteBuf读取的数据都来自这一部分;
  • 第三部分是可写字节,写入到ByteBuf的数据都会写到这一部分;
  • 第四部分是可扩容字节,表示ByteBuf最多还能扩容的大小。

Desktop View ByteBuf定义

ByteBuf定义了三个整型属性,有效地区分可读数据和可写数据的索引,使得读写之间相互没有冲突。这三个属性定义在AbstractByteBuf抽象类中,分别是readerIndex(读指针)、writerIndex(写指针)、maxCapacity(最大容量)。

NIOByteBuffer相比,ByteBuf的优势如下,

  • 读取和写入索引分开(ByteBufNIOByteBuffer进行了改良,增加了一个读写索引,引入读写下标readerIndexwriterIndex,分别用于读写的控制)
  • 支持零复制(比如复合缓冲区类型),减少了内存复制
  • Pooling(池化加速,减少了内存分配和释放,提升了效率)
  • 不需要调用flip()方法去切换读/写模式
  • 可扩展性好
  • 可以自定义缓冲区类型
  • 方法的链式调用
  • 可以进行引用计数,方便重复使用

ByteBuf的分类

从所属的内存区域区分,ByteBufNIOByteBuffer类似,分为堆缓冲区和直接缓冲区,

  • ‌堆缓冲区(HeapByteBuf)‌,将数据存储在JVM堆空间中,特点是在内存的分配和回收速度快,可以被JVM自动回收,缺点是如果进行socketIO读写,需要额外一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降。
  • 直接内存(DirectByteBuf)‌,非堆内存,在堆外进行内存分配,相比于堆内存,分配和回收速度会慢一些,进行socketIO读写将它写入或从socket channel中读取时,由于少一次内存复制,速度比堆内存快。

从内存回收角度看,ByteBuf分为两类,

  • 基于pooledByteBuf,使用内存池进行分配和回收管理,达到重复利用的效果;
  • 普通的ByteBuf,借助JVM的垃圾回收机制进行内存管理。

两者的区别是基于pooledByteBuf可以重复利用,降低由于高负载导致的频繁GC。这里有两个池,对象池和内存池。

ByteBuf的常用类型,HeapDirectpooledUnpooled四个特性之间的组合,如下,

  • UnpooledHeapByteBuf
  • UnpooledDirectByteBuf
  • PooledHeapByteBuf
  • PooledDirectByteBuf

ByteBuf的核心类

核心顶层抽象类是ByteBufAbstractByteBuf继承了ByteBuf类,实现了它的大部分抽象方法,

Desktop View ByteBuf的继承关系

模板模式

AbstractByteBuf模板模式
AbstractByteBuf类中,采用模板方法的设计模式,真正内存读写的细节,以钩子的形式,延迟到子类实现。
父子类的职责划分

  • 父类是默认抽象实现类,负责ByteBuf操作的骨架;
  • 子类负责骨架里面钩子方法的实现。

AbstractByteBuf类负责下标检查和移动,其他公共的操作。比如writerIndexreaderIndex的操作。

不同的子类,内存结构不一样,AbstractByteBuf子类负责真正进行内存读写,比如writeBytesgetBytes。因为各种ByteBuf读写的内存区域不同,读写方式也就有差异。

举个例子,将指定字节数组写入到ByteBuf中,分两步,

  • 把要写的字节src写入到内部的内存结构,setBytes是一个钩子方法,由子类实现;
  • 调整写索引,写的位置由基类方法来完成。
1
2
3
4
5
6
7
8
public ByteBuf writeBytes(byte[] src, int srcIndex, int length)
{
    ensureWritable(length);
    //抽象钩子方法,由不同的子类根据自身的内存类型(堆内、堆外等)实现差异化的读写逻辑
    setBytes(writerIndex, src, srcIndex, length);
    writerIndex += length;
    return this;
}

同样地,读取内容,将ByteBuf内容读取到指定字节数组,

1
2
3
4
5
6
7
8
public ByteBuf readBytes(byte[] dst, int dstIndex, int length)
{
    checkReadableBytes(length);
    //抽象的钩子方法: 真正读内存的getBytes
    getBytes(readerIndex, dst, dstIndex, length);
    readerIndex += length;
    return this;
}

引用计数

AbstractReferenceCountedByteBufAbstractByteBuf的直接子类,提供了引用计数的功能,所有的Netty子类都继承了引用计数基类,其所有的子类都可以使用该功能防止内存泄露。换句话说,NettyByteBuf的内存回收工作是通过引用计数的方式管理的。

Netty之所以采用计数器来追踪ByteBuf的生命周期,一是能对Pooled ByteBuf提供支持,二是能够尽快地发现那些可以回收的ByteBuf(非Pooled),以便提升ByteBuf的分配和销毁的效率。

什么是Pooled(池化)的ByteBuf缓冲区?
Netty4版本开始,新增了ByteBuf的池化机制。即创建一个缓冲区对象池,将没有被引用的ByteBuf对象,放入对象缓存池中;当需要时,则重新从对象缓存池中取出,而不需要重新创建。

引用计数的两个核心方法,

  • retain()将引用计数器加1
  • release()将引用计数器减去1

Netty中,如果引用为0,表示缓冲区不能再继续使用,再次访问这个ByteBuf对象,将会抛出异常;如果引用为0,表示这个ByteBuf没有哪个进程引用它,它占用的内存需要回收。

下面是一个使用引用计数的例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
Logger.info("after create:" + buffer.refCnt());

buffer.retain();
Logger.info("after retain:" + buffer.refCnt());

buffer.release();
Logger.info("after release:" + buffer.refCnt());

buffer.release();
Logger.info("after release:" + buffer.refCnt());

//错误:refCnt: 0,不能再retain
buffer.retain();
Logger.info("after retain:" + buffer.refCnt());

为了确保引用计数不会混乱,在Netty的业务处理器开发过程中,应该坚持一个原则,retainrelease方法应该结对使用。对缓冲区调用了一次retain,就应该调用一次release

怎么保障引用计数的原子性?
引用计数保持在refCnt字段里边,使用字段的原子更新器来做增减。

AbstractReferenceCountedByteBuf的原子操作

  • refCnt:保存引用计数的字段
  • REFCNT_FIELD_OFFSETrefCnt字段在内存中的地址偏移量
  • refCntUpdaterrefCnt字段的原子更新器

Desktop View AbstractReferenceCountedByteBuf类定义

Desktop View AbstractReferenceCountedByteBuf retain

Desktop View AbstractReferenceCountedByteBuf release

AtomicIntegerFieldUpdater是对Object的指定字段进行原子更新。
AtomicIntegerFieldUpdaterJUC包下的工具类,和AtomicInteger等原子变量在一个包下,底层使用的是CAS自旋锁机制,保障操作的原子性。

ByteBuf的浅层复制

ByteBuf的浅层复制分两种,duplicate(整体浅层复制)和slice(切片浅层复制),duplicateslice方法都是浅层复制。不同的是,slice方法是切取一段的浅层复制,而duplicate是整体的浅层复制。

浅层复制方法不会实际去复制数据,也不会改变ByteBuf的引用计数,这就会导致一个问题,在源ByteBuf调用release之后,一旦引用计数为零,就变得不能访问了;在这种场景下,源ByteBuf的所有浅层复制实例也不能进行读写了;如果强行对浅层复制实例进行读写,则会报错。

因此,在调用浅层复制实例时,可以通过调用一次retain方法来增加引用,表示它们对应的底层内存多了一次引用,引用计数为2。在浅层复制实例用完后,需要调用两次release方法,这样就不影响源ByteBuf的内存释放。

派生缓冲区分为池化、非池化两种,有不同的派生关系,派生缓冲区会产生自己的读写索引和其他标记索引,但是与原生缓冲区共享内部的数据(比如引用计数不是独立的)。

Desktop View 派生缓冲区

Desktop View UnpooledDirectByteBuf派生缓冲区

ByteBuf的自动创建和自动释放

ByteBuf的自动创建、自动释放过程,被Netty封装起来了。

自动创建

首先找到ByteBuf自动创建的最源头(Channel入站数据的读取,NioSocketChannel),数据进入到通道后,数据就是通道数据,怎么从通道中读取出来,这个问题回顾下前面的NioEventLoop反应器核心原理。

processSelectedKeys里边,第一个参数就是选择键实例,第二个参数是选择键的附件(事件所属的通道),ch.unsafe()方法,拿到Netty内部Nio通道,NioUnsafe专门用来做Java通道的读写,事件类型的判断,选择键是可写的,就对JavaNIO通道做刷新操作,选择键是可读的,就做JavaNIO通道的读取工作,

Desktop View NioEventLoop.processSelectedKey事件分发环节

这里就是入站数据最源头,

  • config方法获取通道配置示例;
  • pipeline获取到通道处理器的流水线;
  • config.getAllocator方法,使用通道配置实例,获取到内存分配器;
  • 前面准备工作做好后,读取数据之前,直接分配bytebuf(存放数据的缓冲区),分配大小根据公式推断出合理的大小,进行通道数据读取,从socket里边接收数据。

Desktop View OP-READ事件对应的NioByteUnsafe.read方法实现

上面的方法,通过通道获取到了很多属性,NettyJDK原生的ServerSocketChannel进行了封装和增强,拥有了如下的属性,

  • id,标识唯一身份信息
  • 可能存在的parent Channel
  • 管道,pipeline
  • 用于数据读写的unsafe内部类,如(封装JDKJdkChannel
  • 关联上相伴终生的NioEventLoop(反应器)
  • channelconfig配置实例

内存分配器,可以在引导类上面进行配置,这里使用的是非池化的内存分配器,这里配置上之后,就可以在前面的事件处理方法中直接调用到,

Desktop View ChannelConfig通道配置类示例

doReadBytes方法,这个方法就是把Java通道的数据读取到ByteBuf里,这个Java通道就是被Netty通道封装起来的,完成入站数据的读取,

Desktop View 读取NioSocketChannel

read方法的后半部分,把读取到的ByteBuf,通过Channel的处理器流水线分发出去,进入流水线进行Handler处理,不是只读一次,会判断进行持续读取,流水线Handler处理,就是NettyHandler开发,

  • 触发pipeline.fireChannelRead(byteBuf)(通过pipeline触发读事件)。
  • 判断是否继续读:有两个标准,
    • 一是不能超过最大的读取次数(默认16次);
    • 二是缓冲区的数据每次都要读满,比如分配2KB ByteBuf,则必须读取2KB的数据。

Desktop View 关注NioByteUnsafe.read方法实现的后半部分

ByteBuf的后续观察中,用到了内存缓冲区的分配器。分配器在分配内存缓冲区时,会调用ioBuffer方法,会根据操作系统平台的环境进行判断,是不是有unsafe类,如果没有,就直接分配堆内存,如果有,就直接分配直接缓冲区。

Desktop View AbstractByteBufAllocator ioBuffer

分配直接缓冲区,一类是池化,一类是非池化。默认情况下,进入到池化的分配器子类,分配池化的直接缓冲区,

Desktop View PooledByteBufAllocator newDirectBuffer

总结下NioByteUnsafe.read方法的工作,

  • 分配缓冲区:默认1024 Byte,之后根据最近几次请求的数据包大小,猜测下一次数据包大小;
  • 读取数据:直接调用Java NIO的底层代码;
  • 触发pipeline.fireChannelRead(byteBuf):开始pipeline流水线的业务处理;
  • 判断是否继续读:有两个标准,一是不能超过最大的读取次数(默认16次);二是缓冲区的数据每次都要读满,比如分配2KB ByteBuf,则必须读取2KB的数据。

自动释放

第一种,流水线尾部的自动释放

如果流水线前面的处理器没有对ByteBuf释放,尾部的最后一个环节,会做一个释放工作。当然,如果在流水线的某一个环节,把ByteBuf释放掉,尾部的也不会再释放。

Desktop View 流水线尾部的自动释放(图片来自网络)

怎么让ByteBuf到达尾部? 通过调用super.channelRead方法,一路传递到尾部。

第二种,处理器不是继承于ChannelInboundHandler,而是继承于SimpleChannelInboundHandlerNetty自定义的基类)

SimpleChannelInboundHandler基类帮我们做了释放工作,继承了这个基类,业务代码是写在channelRead0方法里边的,不是写在channelRead里边。

第三种,出站处理时,自动释放

之前都是从前向后,现在这种是从后往前,最后会走到HeadContext,但通道的写入最后会在AbstractNioByteChanneldoWrite方法,把ByteBuf内容写入到被封装的底层Java NIO通道,然后通过底层的IOUtils方法,把数据写入到直接内存,然后写入到内核缓冲区,直接发送出去。发送完之后,就会调用in.remove方法,这里就会释放ByteBuf

Desktop View 出站处理时,自动释放(图片来自网络)

Desktop View AbstractNioByteChannel doWrite方法

Desktop View AbstractNioByteChannel doWriteInternal方法

Desktop View ChannelOutboundBuffer remove方法

UnpooledHeapByteBuf非池化的堆内存

非池化的堆内存,一个非池化的、内存空间分配在堆的ByteBuf,是ByteBuf最简单的一个实现类。

Desktop View ByteBuf的继承关系

UnpooledHeapByteBuf的关键属性如下,

1
2
3
4
5
6
7
8
9
10
// ByteBuf的分配器,ByteBufAllocator接口的所有实现类都是线程安全的
private final ByteBufAllocator alloc;

// 支撑数组,堆内字节缓冲,本质上是字节数组的封装
// 数据都存放在这个数组里
byte[] array;

// JDK Nio包中的ByteBuffer对象
// 主要用来做一些临时性的操作
private ByteBuffer tmpNioBuf;

UnpooledHeapByteBuf内存操作的钩子实现

回到模板的抽象基类,AbstractByteBuf子类负责骨架里面钩子方法的实现,比如setBytesgetBytes方法。

setBytes使用arraycopy方法,通过数组拷贝的方式,直接将源字节流,拷贝到UnpooledHeapByteBuf对象的array属性中,

Desktop View UnpooledHeapByteBuf setBytes方法

getBytes则是将内部的字节数组array,拷贝到目标数组dst

Desktop View UnpooledHeapByteBuf getBytes方法

UnpooledHeapByteBuf的内存分配

UnpooledByteBuf的创建,不建议使用构造器创建,而是使用Allocator分配器创建。

UnpooledByteBuf默认的Allocator分配器,UnpooledByteBufAllocator.DEFAULT

Desktop View UnpooledByteBufAllocator创建用例

运行结果如下,

1
2
3
4
5
6
7
8
9
10
after ===========allocate ByteBuf(9, 100)============
capacity(): 9
maxCapacity(): 100
readerIndex(): 0
readableBytes(): 0
isReadable(): false
writerIndex(): 0
writableBytes(): 9
isWritable(): true
maxWritableBytes(): 100

具体做分配时,进一步调用钩子函数,最终通过newHeapBuffer来创建UnpooledHeapByteBuf实例,

Desktop View AbstractByteBufAllocator heapBuffer方法

newHeapBuffer,由子类实现,

Desktop View UnpooledByteBufAllocator newHeapBuffer方法

UnpooledByteBufAllocator的内部类,根据PlatformDependent.hasUnsafe进行区分,创建不同的子类对象,

  • 内部类InstrumentedUnpooledHeapByteBuf继承了UnpooledHeapByteBuf
  • 内部类InstrumentedUnpooledUnsafeHeapByteBuf继承了UnpooledUnsafeHeapByteBuf

InstrumentedUnpooledUnsafeHeapByteBufInstrumentedUnpooledHeapByteBufUnpooledByteBufAllocator的内部类。

这里重点关注UnpooledHeapByteBufUnpooledUnsafeHeapByteBuf

Desktop View UnpooledHeapByteBuf子类的继承关系

UnpooledUnsafeHeapByteBufUnpooledHeapByteBuf的不同之处,在于allocateArray分配字节数组的实现不同。

  • UnpooledUnsafeHeapByteBuf
    • 优先使用性能更好的jdk.internal.misc.Unsafe类去分配和释放字节数组
  • InstrumentedUnpooledHeapByteBuf
    • 使用new byte[]创建byte数组。

UnpooledUnsafeHeapByteBuf

  • UnpooledUnsafeHeapByteBuf对字节数组的分配和释放,也通过Unsafe类来进行,
    • jdk.internal.misc.Unsafe.allocateUninitializedArray(int)
  • UnpooledUnsafeHeapByteBuf后续对字节数组的读写,也通过Unsafe类来进行
    • 通过这种方式进行字节数组的读写,据说性能也会高些。 除此之外,UnpooledUnsafeHeapByteBufUnpooledHeapByteBuf没有其他差别。

JDK 1.8没有allocateUninitializedArray这个方法,这个方法有门槛,

通过Unsafe.allocateUninitializedArray来分配数组的条件,只有JDK版本为9以上并且持有jdk.internal.misc.Unsafe类,所以对于Java 8来说,还是通过new byte[]创建byte数组的方式分配内存。

对于Java 9及以上版本HotSpot虚拟机来说一般都会实例化这个类。如果不是Java 9以上版本,或者没有jdk.internal.misc.Unsafe,那么默认实例化InstrumentedUnpooledHeapByteBuf

这里的使用的Unsafe类不是JDK1.8中的sun.misc.Unsafe类,而是jdk.internal.misc.Unsafe类。

Netty里边,对这些版本有做判断,PlatformDependent.hasUnsafe进行Unsafe存在性判定,

Desktop View 版本有做判断

PlatformDependent0.ALLOCATE_ARRAY_METHOD保持Unsafe的内存分配方法,ALLOCATE_ARRAY_METHOD常量,以反射式的模式初始化,初始化为以下方法,用于后续进行快速的堆内存分配。

1
jdk.internal.misc.Unsafe.allocateUninitializedArray(int)

内存分配钩子allocateArray的实现

接下来进入正题,介绍内存分配钩子allocateArray的实现。

基类UnpooledHeapByteBuf的内存分配钩子allocateArray如下,

1
2
3
protected byte[] allocateArray(int initialCapacity) {
    return new byte[initialCapacity]; // 默认分配字节数组
}

两个子类,都重写了这个分配钩子,

  • InstrumentedUnpooledHeapByteBuf
  • InstrumentedUnpooledUnsafeHeapByteBuf

UnpooledHeapByteBuf的钩子实现就很简单,如上所示,InstrumentedUnpooledHeapByteBuf这个什么都没做,直接调用了UnpooledHeapByteBuf基类的数组分配方法,堆内存的内存数量增加了一个数组长度,做统计计数,

Desktop View InstrumentedUnpooledHeapByteBuf的内存分配钩子allocateArray

InstrumentedUnpooledUnsafeHeapByteBuf这个则是完整的重写了,没有调用UnpooledHeapByteBuf基类的,

Desktop View InstrumentedUnpooledUnsafeHeapByteBuf的内存分配钩子allocateArray

根据一个门禁常量判断有没有Unsafe,去执行不同类型的方法分配内存,

Desktop View PlatformDependent allocateUninitializedArray,根据门禁进行数组分配

Desktop View UNINITIALIZED ARRAY ALLOCATION THRESHOLD的门禁取值

UNINITIALIZED_ARRAY_ALLOCATION_THRESHOLD门禁条件,

  • JDK版本为9以上
  • 并且持有jdk.internal.misc.Unsafe类的allocateUninitializedArray
  • 否则,UNINITIALIZED_ARRAY_ALLOCATION_THRESHOLD的值为-1

PlatformDependent0通过unsafe分配字节数组

Desktop View PlatformDependent0通过unsafe分配字节数组

总之,UnpooledUnsafeHeapByteBuf的数组分配和平台有关,

  • 如果使用了Java 9以上版本,并且非安卓,UnpooledUnsafeHeapByteBufallocateArray通过Unsafe来分配初始的字节数组,
    1
    
    jdk.internal.misc.Unsafe.allocateUninitializedArray(int)
    
  • 其他情况下,直接创建byte数组并返回。

UnpooledHeapByteBuf是一个基于JVM堆内存进行内存分配的缓冲区,是一个非池化的实现。在IO读写的时候多一次从Linux用户空间内存和JVM内存之间的复制过程,在每次IO读写的时候都会创建一个UnpooledHeapByteBuf对象。具有以下特点,

  • 多了一次内存复制,UnpooledHeapByteBufIO读写的场景下性能较低;
  • 如果需要频繁进行内存分配和回收,但相比于堆外内存申请和释放,成本要低一些。

NIO的DirectByteBuffer

在开始UnpooledDirectByteBuf之前,必须先介绍一下NIODirectByteBuffer

看一个UnpooledDirectByteBuf的例子,调试查看UnpooledDirectByteBuf的内部,可以看到NettyUnpooledDirectByteBuf封装了NIODirectByteBuffer

Desktop View UnpooledDirectByteBuf封装了NIO的DirectByteBuffer

JDK1.4中开始,我们可以使用native方法在直接内存上来分配内存,并在JVM堆内存上维持一个引用来进行访问,当JVM堆内存上的引用被回收后,这块内存被操作系统回收。

具体来说,

  • 直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
  • 使用native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
  • 这样能在一些场景中显著提高性能,因为避免了在Java堆中和native堆中来回复制数据。

直接内存与JVM堆内存的概念区别

  • JVM堆内存‌
    • 堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。
  • 堆外内存(直接内存)是相对于堆内内存的一个概念,堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。

使用NIO ByteBuffer来分配直接内存,放5个数,转换模式后,读取出来,

Desktop View NIO DirectByteBuffer的例子

Desktop View NIO DirectByteBuffer的继承关系

DirectByteBufferJava用于实现堆外内存的一个重要类,我们可以通过该类实现堆外内存的创建、使用和销毁。

  • DirectByteBuffer的创建
  • DirectByteBuffer的性能优势
  • 如何设置堆内存的大小
  • DirectByteBuffer创建原理
  • DirectByteBuffer释放原理
  • Unsafe的内存分配和释放方法
  • 堆外内存的使用场景

DirectByteBuffer的创建方法

类型方法
堆缓冲(堆内存)ByteBuffer.allocate(int capacity)
直接缓冲区(直接内存)ByteBuffer.allocateDirect(int capacity)
堆缓冲(堆内存)ByteBuffer.wrap(byte[] array)
堆缓冲(堆内存)ByteBuffer.wrap(byte[] array, int offset, int length)

使用ByteBuffer静态类的allocateDirect静态方法,创建直接缓冲区ByteBuffer,其他三个都是堆内存,

  • allocate方式创建的ByteBuffer对象,我们称之为非直接缓冲区,这个ByteBuffer对象(和对象包含的缓冲数组)都位于JVM的堆区。
  • allocateDirect方法创建的ByteBuffer,我们称之为直接缓冲区,此时ByteBuffer对象本身在堆区,而缓冲数组位于非堆区,ByteBuffer对象内部存储了这个非堆缓冲数组的地址。
  • wrap方式和数组创建方式创建的ByteBuffer没有本质区别,都创建的是非直接缓冲区。

DirectByteBuffer的性能优势

DirectByteBuffer在非堆区的缓冲数组可以通过JNI(底层使用系统调用)方式进行IO操作,性能优势如下,

  • JNI不受GC影响,机器码执行速度也比较快;
  • 还避免了JVM堆区与进程用户空间缓冲区的数据拷贝,所以IO速度比非直接缓冲区快。

也有一些劣势,

  • allocateDirect方式创建ByteBuffer对象花费的时间;
  • 回收该对象花费的时间比较多,所以这个方法适用于创建那些需要重复使用的缓冲区对象。

创建和回收该直接缓冲区对象性能低,为什么?
创建和回收内存的时候,是通过JNI系统调用来完成的,需要进行内核态和用户态之间的切换,比如分配内存用的Cmalloc方法。
一般情况下,allocateDirectJNI实现,涉及到C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换。

Java程序里边,怎么配置堆外内存的大小? 默认情况下,堆外内存和JVM内存大小相同,

Desktop View 获取堆外内存的大小

Desktop View 默认与JVM堆大小相同,设置-Xmx100M

配置的最大堆内存是100M,实际拿到的最大堆内存是96M,为什么?
新生代占1/3,老生代占2/3
新生代分3个区,一个是Eden,一个是Survivor,两个Survivor是来回复制的,所以只能算一个,EdenSurvivor比例8:1:1EdenSurvivor9/10
maxMemory = Eden + Survivor + Old Gen96M = 30M + 66 M

如何设置堆内存的大小:-XX:MaxDirectMemorySize=1024M

DirectByteBuffer创建原理

DirectByteBuffer的构造函数主要做以下三个事情,

  • 根据页对齐和pageSize来确定本次的要分配内存实际大小
  • 实际分配内存,并且记录分配的内存大小
  • 声明一个Cleaner对象用于清理该DirectBuffer内存

Desktop View DirectByteBuf的构造函数

传进来实际申请的数量,计算一个实际分配的数量,不是传进来申请多少,就分配多少,还得考虑内存对齐,尺寸大小的计算。

DirectByteBuffer的释放

DirectByteBuffer内存回收主要有以下方式,

  • 通过FullGC来自动回收
  • 通过手动System.gc来回收
  • 另一种是通过构造函数里创建的Cleaner对象来回收
  • 通过Unsafe.freeMemory方法直接释放

看个DirectByteBuf FullGC的例子,虚拟机配置如下,

1
2
3
// 直接内存设置最大1G
// 打印GC日志
-XX:MaxDirectMemorySize=1024M -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails

JVM发现直接内存MaxDirectMemorySize=1024M所剩无几的时候,就会触发FullGC

Desktop View DirectByteBuffer FullGC的例子

现在把直接内存在JVM里设置在1G,每次申请250M内存,可以发现3个循环之后会触发一次FullGCGC日志输出如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
VM.maxDirectMemory() 1024.0 MB
[Full GC (System.gc()) [CMS: 0K->1477K(87424K), 0.1183745 secs] 11007K->1477K(126720K), [Metaspace: 5065K->5065K(1056768K)], 0.1195972 secs] [Times: user=0.03 sys=0.09, real=0.12 secs] 
[Full GC (System.gc()) [CMS: 1477K->1137K(87424K), 0.0406728 secs] 1478K->1137K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0419954 secs] [Times: user=0.03 sys=0.02, real=0.04 secs] 
[Full GC (System.gc()) [CMS: 1137K->1134K(87424K), 0.0190550 secs] 1839K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0192406 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0113586 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0115312 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0191127 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0192708 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0124816 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0126392 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0157665 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0159360 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0116543 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0118291 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0108858 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0110087 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0123029 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0124361 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0078329 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0079431 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0082095 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0083218 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0112228 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0113256 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0076363 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0078986 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0086060 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0087987 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0093434 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0094636 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0120732 secs] 1134K->1134K(126848K), [Metaspace: 5068K->5068K(1056768K)], 0.0122867 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0099590 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0101044 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0142404 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0143844 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0097119 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0098441 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0102243 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0103650 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0109815 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0111438 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0091188 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0092453 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0100537 secs] 1836K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0102063 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0119528 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0120607 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0092390 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0093926 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0086915 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0088150 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0102697 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0104111 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0108851 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0110153 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0123193 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0124395 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0083704 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0084688 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1134K->1134K(87424K), 0.0081445 secs] 1134K->1134K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0083285 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
程序运行时间: 15003ms
Heap
 par new generation   total 39424K, used 701K [0x0000000081a00000, 0x00000000844c0000, 0x00000000ab390000)
  eden space 35072K,   2% used [0x0000000081a00000, 0x0000000081aaf728, 0x0000000083c40000)
  from space 4352K,   0% used [0x0000000083c40000, 0x0000000083c40000, 0x0000000084080000)
  to   space 4352K,   0% used [0x0000000084080000, 0x0000000084080000, 0x00000000844c0000)
 concurrent mark-sweep generation total 87424K, used 1134K [0x00000000ab390000, 0x00000000b08f0000, 0x0000000100000000)
 Metaspace       used 5088K, capacity 5242K, committed 5504K, reserved 1056768K
  class space    used 567K, capacity 594K, committed 640K, reserved 1048576K

Desktop View DirectByteBuffer通过手动System.gc()来回收

System.gc()调用会显式地触发FullGC,同时对老年代和新生代、直接内存进行回收,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
VM.maxDirectMemory() 1024.0 MB
[Full GC (System.gc()) [CMS: 0K->1477K(87424K), 0.0388643 secs] 11007K->1477K(126720K), [Metaspace: 5056K->5056K(1056768K)], 0.0390786 secs] [Times: user=0.02 sys=0.03, real=0.04 secs] 
[Full GC (System.gc()) [CMS: 1477K->1322K(87424K), 0.0127459 secs] 1490K->1322K(126848K), [Metaspace: 5057K->5057K(1056768K)], 0.0128895 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1322K->1144K(87424K), 0.0167491 secs] 2037K->1144K(126848K), [Metaspace: 5058K->5058K(1056768K)], 0.0169601 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0119271 secs] 1859K->1144K(126848K), [Metaspace: 5059K->5059K(1056768K)], 0.0126592 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0116094 secs] 1157K->1144K(126848K), [Metaspace: 5061K->5061K(1056768K)], 0.0119911 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0102302 secs] 1157K->1144K(126848K), [Metaspace: 5062K->5062K(1056768K)], 0.0104904 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0119499 secs] 1157K->1144K(126848K), [Metaspace: 5069K->5069K(1056768K)], 0.0121445 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0105994 secs] 1157K->1144K(126848K), [Metaspace: 5070K->5070K(1056768K)], 0.0107187 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0113304 secs] 1157K->1144K(126848K), [Metaspace: 5070K->5070K(1056768K)], 0.0114684 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0104194 secs] 1157K->1144K(126848K), [Metaspace: 5070K->5070K(1056768K)], 0.0106316 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0128884 secs] 1851K->1144K(126848K), [Metaspace: 5070K->5070K(1056768K)], 0.0130314 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0151508 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0153258 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0175015 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0176691 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0157848 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0160059 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0119696 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0121271 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0132510 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0134609 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0157065 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0158796 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0150871 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0152505 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0173008 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0174803 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0160453 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0162010 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0130528 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0132114 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0115966 secs] 1846K->1144K(126848K), [Metaspace: 5071K->5071K(1056768K)], 0.0117359 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0125462 secs] 1846K->1144K(126848K), [Metaspace: 5072K->5072K(1056768K)], 0.0127110 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0107178 secs] 1846K->1144K(126848K), [Metaspace: 5073K->5073K(1056768K)], 0.0108750 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0121459 secs] 2547K->1144K(126848K), [Metaspace: 5073K->5073K(1056768K)], 0.0122771 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0112280 secs] 1846K->1144K(126848K), [Metaspace: 5073K->5073K(1056768K)], 0.0114169 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0090373 secs] 1846K->1144K(126848K), [Metaspace: 5073K->5073K(1056768K)], 0.0092042 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0102973 secs] 1846K->1144K(126848K), [Metaspace: 5073K->5073K(1056768K)], 0.0104285 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0116177 secs] 1846K->1144K(126848K), [Metaspace: 5073K->5073K(1056768K)], 0.0117482 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0113139 secs] 1846K->1144K(126848K), [Metaspace: 5073K->5073K(1056768K)], 0.0114889 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0125177 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0126617 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0118334 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0120065 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0137146 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0138653 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0176295 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0178353 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0168078 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0169966 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0198929 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0200726 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0173743 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0176300 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0188363 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0190534 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0197586 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0199219 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0170999 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0172787 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0154172 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0155388 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0149408 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0150700 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0116016 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0117469 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0113373 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0115147 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0114547 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0115811 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0133796 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0135362 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0150022 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0151837 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0165591 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0167208 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0144162 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0145845 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1144K(87424K), 0.0148112 secs] 1846K->1144K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0149734 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1144K->1145K(87424K), 0.0185839 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0187494 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0182045 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0184219 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0177225 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0179134 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0178189 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0180543 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0140190 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0142418 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1147K(87424K), 0.0176175 secs] 1846K->1147K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0178323 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1147K->1146K(87424K), 0.0138031 secs] 1848K->1146K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0139799 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1146K->1145K(87424K), 0.0143173 secs] 1848K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0145060 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1146K(87424K), 0.0143045 secs] 1846K->1146K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0144551 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1146K->1147K(87424K), 0.0146641 secs] 1847K->1147K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0148633 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1147K->1148K(87424K), 0.0166404 secs] 1848K->1148K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0167807 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1148K->1145K(87424K), 0.0190820 secs] 1849K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0194078 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0171780 secs] 1847K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0174187 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0185020 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0186907 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0167993 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0169282 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0177797 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0179099 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0168266 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0169756 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0132231 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0133741 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0129281 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0130592 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0119076 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0120189 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1150K(87424K), 0.0109614 secs] 1846K->1150K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0111364 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1150K->1145K(87424K), 0.0133880 secs] 1851K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0135588 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0136729 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0138535 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0165481 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0167231 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0199082 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0201061 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1152K(87424K), 0.0175450 secs] 1846K->1152K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0179324 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1152K->1145K(87424K), 0.0180362 secs] 1854K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0182399 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1152K(87424K), 0.0173960 secs] 1846K->1152K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0175724 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1152K->1145K(87424K), 0.0183213 secs] 1854K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0184789 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1152K(87424K), 0.0157460 secs] 1846K->1152K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0159655 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1152K->1145K(87424K), 0.0154369 secs] 1854K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0156180 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0144819 secs] 1847K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0146510 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0128476 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0129976 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0146637 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0148493 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0146661 secs] 1846K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0148100 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1152K(87424K), 0.0141918 secs] 1847K->1152K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0143431 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1152K->1145K(87424K), 0.0154548 secs] 1854K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0157413 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0169995 secs] 1847K->1145K(126848K), [Metaspace: 5074K->5074K(1056768K)], 0.0172008 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0190515 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0192372 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0188673 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0191239 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0186910 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0188792 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0192899 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0195104 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0145434 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0147364 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0140586 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0141754 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0137579 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0139115 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0133921 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0135673 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0111670 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0112989 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0149480 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0151831 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0144447 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0146719 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [CMS: 1145K->1145K(87424K), 0.0139020 secs] 1847K->1145K(126848K), [Metaspace: 5075K->5075K(1056768K)], 0.0140795 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
程序运行时间: 1616ms
Heap
 par new generation   total 39424K, used 701K [0x0000000081a00000, 0x00000000844c0000, 0x00000000ab390000)
  eden space 35072K,   2% used [0x0000000081a00000, 0x0000000081aaf5f8, 0x0000000083c40000)
  from space 4352K,   0% used [0x0000000083c40000, 0x0000000083c40000, 0x0000000084080000)
  to   space 4352K,   0% used [0x0000000084080000, 0x0000000084080000, 0x00000000844c0000)
 concurrent mark-sweep generation total 87424K, used 1145K [0x00000000ab390000, 0x00000000b08f0000, 0x0000000100000000)
 Metaspace       used 5094K, capacity 5242K, committed 5504K, reserved 1056768K
  class space    used 568K, capacity 594K, committed 640K, reserved 1048576K

System.gc()FGC的问题

  • ‌System.gc()不靠谱‌
    • 大部分调优指南都设置了参数-XX:+DisableExplicitGC来禁用System.gc()
    • System.gc()命令大部分情况被禁用。System.gc()会显式直接触发Full GC,同时对老年代和新生代、直接内存进行回收。而一般情况下我们认为,垃圾回收应该是自动进行的,无需手工触发。如果过于频繁地触发垃圾回收对系统性能是没有好处的。因此虚拟机提供了一个参数DisableExplicitGC来控制是否手工触发GC
  • ‌依赖老年代FGC也是不靠谱的‌
    • 只有堆内的对象(DirectByteBuffer)回收之后,才会顺带回收直接内存,反过来,仅触发直接内存回收不会回收堆内对象。解决方案:手动释放。
  • JDKDirectByteBuffer中提供了Cleaner用来主动释放内存。
  • 同时还有UnsafefreeMemory方法也可以实现手动释放。

通过反射直接拿到直接内存Cleaner实例,调用clean方法,

Desktop View 使用Cleaner手动来回收内存

DirectByteBuffer通过FullGC来回收内存的,DirectByteBuffer会自己检测情况而调用system.gc(),但是如果参数中使用了DisableExplicitGC那么就无法回收该块内存了,-XX:+DisableExplicitGC标志自动将System.gc()调用转换成一个空操作,就是应用中调用System.gc()会变成一个空操作。那么,如果设置了就需要我们手动来回收内存了。

Cleaner对象回收的原理

在每次新建一个DirectBuffer对象的时候,会同时创建一个Cleaner对象,同一个进程创建的所有DirectBuffer对象跟Cleaner对象的个数是一样的,并且所有的Cleaner对象会组成一个链表,前后相连。

1
2
3
4
5
public static Cleaner create(Object ob, Runnable thunk) {
    if (thunk == null)
        return null;
    return add(new Cleaner(ob, thunk));
}

Cleaner对象的clean方法执行时机是,JVM在判断该Cleaner对象关联的DirectBuffer已经不被任何对象引用了(也就是经过可达性分析判定为不可达的时候)。此时Cleaner对象会被JVM挂到PendingList上。然后有一个固定的线程扫描这个List,如果遇到Cleaner对象,那么就执行clean方法。

Cleaner对象回收的问题:存在滞后性。默认情况下,Netty不是用Cleaner来释放内存的。

通过Unsafe.freeMemory方法直接释放

如果通过Unsafe.freeMemory方法直接释放,那么需要使用DirectByteBuffer如下的构造器,

Desktop View DirectByteBuf的构造函数

前提是需要reallocateMemory(long address, long size);提前分配好内存。

这种方式不再需要Cleaner,构造器变了,addr参数是直接内存在用户空间里的内存地址。

sun.misc.Unsafe类的方法

sun.misc.Unsafe
Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如NettyCassandraHadoopKafka等。
Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。

sun.misc.Unsafe提供了一组JNI方法来进行堆外内存的分配,重新分配,以及释放。这些方法里边分配内存,释放内存,都是用户空间里边的内存地址,不是JVM堆里边的内存,

1
2
3
4
5
6
7
8
9
10
// 分配一块内存空间
public native long allocateMemory(long size);

// 重新分配一块内存,把数据从 address 指向的缓存中拷贝到新的内存块
public native long reallocateMemory(long address, long size);

// 释放内存
public native void freeMemory(long address);

// 以上方法也不是静态的,不能通过Unsafe.allocateMemory调用

获取Unsafe实例的方法,在使用Unsafe类的时候,

1
Unsafe f = Unsafe.getUnsafe();

发现还是被拒绝了,抛出异常,

1
java.lang.SecurityException: Unsafe

只能使用反射来做这件事,

1
2
3
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);

Desktop View Unsafe的例子

释放这部分内存的话,需要调用freeMemory或者reallocateMemory方法。

DirectByteBuffer使用sun.misc.Unsafe类申请直接内存,

Desktop View DirectByteBuf的构造函数

DirectByteBuffer使用sun.misc.Unsafe类释放直接内存,

Desktop View DirectByteBuf的释放内存

堆外内存的使用场景

使用堆外内存的原因

  • 对垃圾回收停顿的改善:因为FullGC意味着彻底回收,彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这意味着一个重要的事实,这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理(而不是虚拟机)。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。
  • 在某些场景下可以提升程序IO操纵的性能,省去了将数据从堆内内存拷贝到堆外内存的步骤。

什么情况下使用堆外内存?

  • 堆外内存适用于生命周期中等或较长的对象(如果是生命周期较短的对象,在YGC的时候就被回收了,就不存在大内存且生命周期较长的对象在FGC对应用造成的性能影响)。
  • 直接的文件拷贝操作,或者IO操作。直接使用堆外内存就能省去内存从用户内存拷贝到系统内存的操作,因为IO操作是系统内核内存和设备间的通信,而不是通过程序直接和外设通信的。
  • 同时,还可以使用池 + 堆外内存的组合方式,来对生命周期较短,但涉及到IO操作的对象进行堆外内存的再使用。(Netty中就使用了该方式)

堆外内存,创建和释放比较耗时间,需要做到尽可能地复用,所以需要使用内存池来做内存管理。在Netty中,除了使用内存池,还使用了对象池,对象池主要用于两类对象,

  • 生命周期较短,且结构简单的对象,在内存池中重复利用这些对象能增加CPU缓存的命中率,从而提高性能;
  • 加载含有大量重复对象的大片数据,此时使用内存池能减少垃圾回收的时间。

堆外内存和内存池一样,也能缩短垃圾回收时间,但是它适用的对象和内存池完全相反。内存池往往适用于生命期较短的可变对象,而生命期中等或较长的对象,正是堆外内存要解决的。

堆外内存的特点

  • 对于大内存有良好的伸缩性
  • 对垃圾回收停顿的改善可以明显感觉到
  • 在进程间可以共享,减少虚拟机间的复制

堆外内存的一些问题

  • 堆外内存回收问题,不受JVM内存回收管理,分配回收成本较高
  • 以及堆外内存的泄漏问题
    • 因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
  • 堆外内存的数据结构问题

堆外内存最大的问题,就是你的数据结构变得不那么直观,如果数据结构比较复杂,就要对它进行串行化(serialization),而串行化本身也会影响性能。另一个问题是由于你可以使用更大的内存,你可能开始担心虚拟内存(即硬盘)的速度对你的影响了。

UnpooledDirectByteBuf非池化的直接内存

Netty非池化的直接内存

直接内存DirectByteBuffer
DirectByteBuffer,顾名思义是直接内存(Direct Memory)上的Byte缓存区,直接内存不是JVM Runtime数据区域的一部分,也不是Java虚拟机规范中定义的内存区域。简单的说,这部分就是机器内存,分配的大小等都和虚拟机限制无关。

Desktop View UnpooledDirectByteBuf noUnsafe的例子

AbstractByteBufAllocatordirectBuffer方法,用来分配直接内存类型的ByteBuf缓冲区,是Netty实现堆外内存操作的关键逻辑入口。

Desktop View DirectByteBuf的directBuffer方法

Desktop View UnpooledByteBufAllocator newDirectBuffer

两种平台场景

  • PlatformDependent.hasUnsafe()
    • hotspot JVM,普通的JVM
  • !PlatformDependent.hasUnsafe()
    • 安卓的JVMSystem.setProperty("java.vm.name","Dalvik");
    • 或者配置环境变量,System.setProperty("io.netty.noUnsafe","true");

PlatformDependentPlatformDependent0

  • 主要针对操作系统、JDK版本等环境因素进行判断,判断是否支持堆外内存Unsafe以及一些关联类;
  • 通过封装Unsafe申请堆外内存、释放、获取数据等操作。

!PlatformDependent.hasUnsafe()场景

UnpooledDirectByteBuf的内存分配

!PlatformDependent.hasUnsafe()这种场景,UnpooledDirectByteBuf构造函数会调用分配内存的钩子方法allocateDirect方法,

Desktop View UnpooledDirectByteBuf构造函数

Desktop View 子类的allocateDirect钩子实现

UnpooledDirectByteBuf的默认allocateDirect内存分配,在allocateDirect方法中,分配NIO直接内存。

Desktop View UnpooledDirectByteBuf的默认allocateDirect内存分配

Desktop View UnpooledDirectByteBuf的继承关系

UnpooledDirectByteBuf的关键属性如下,

1
2
3
4
5
6
7
8
9
10
11
//内部字节Java NIO buf,对java.nio.DirectByteBuffer的封装
private ByteBuffer buffer;

//临时nio buf
private ByteBuffer tmpNioBuf;

//容量
private int capacity;

//是否需要释放内存
private boolean doNotFree;

封装了NIODirectByteBuffer,调用其构造器进行创建时,就会创建java.nio.DirectByteBuffer,从而分配到位于堆外的直接内存。

分配内存的时候,委托给NIOByteBuffer分配直接内存(allocateDirect方法),

1
2
3
protected ByteBuffer allocateDirect(int initialCapacity) {
    return ByteBuffer.allocateDirect(initialCapacity);
}

java.nio.ByteBufferallocateDirect方法,如下,

1
2
3
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

NIOByteBuffer的知识很重要,在Android平台,或者没有Unsafe类的情况下,NettyDirectByteBuf封装了NIOByteBuffer API,这个场景比较简单。

UnpooledDirectByteBuf内存操作的钩子实现

回到模板的抽象基类,AbstractByteBuf子类负责骨架里面钩子方法的实现,比如setBytesgetBytes方法。

AbstractByteBuf writeBytes将指定字节数组写入到本ByteBuf中,UnpooledDirectByteBufsetBytes钩子实现

1
2
3
4
5
6
7
8
9
10
public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
    checkSrcIndex(index, length, srcIndex, src.length);

    //UnpooledDirectByteBuf的读写都转换为ByteBuffer的读写
    ByteBuffer tmpBuf = internalNioBuffer();

    tmpBuf.clear().position(index).limit(index + length);
    tmpBuf.put(src, srcIndex, length);
    return this;
}

代码中的internalNioBuffer()方法创建出一个tmpBuf,这个tmpBuf和源ByteBuffer共享原来的内存段,但是有独立的下标。

internalNioBuffer()
tmpBuf是源ByteBuffer的浅层复制,对tmpBuf进行写入,变更会反映到源buffer属性。

Desktop View UnpooledDirectByteBuf internalNioBuffer方法

UnpooledDirectByteBufwriterIndex有谁负责修改?
writerIndex的修改在父类writeBytes模板方法中完成。

UnpooledDirectByteBufgetBytes钩子实现,getBytes也是转换为对NIO ByteBuffer的读取,

1
2
3
4
5
6
7
8
9
10
11
12
13
private void getBytes(int index, ByteBuffer dst, boolean internal) {
    checkIndex(index, dst.remaining());

    ByteBuffer tmpBuf;
    if (internal) {
        tmpBuf = internalNioBuffer();
    } else {
        tmpBuf = buffer.duplicate();
    }
    // 使用clear方法,从写模式转成读取模式
    tmpBuf.clear().position(index).limit(index + dst.remaining());
    dst.put(tmpBuf);
}

PlatformDependent.hasUnsafe()场景

PlatformDependent.hasUnsafe()这种场景,分配内存会区分有无Cleaner

Desktop View UnpooledDirectByteBuf Unsafe的例子

Desktop View UnpooledByteBufAllocator newDirectBuffer的钩子实现

  • noCleanertrue:创建InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf对象,简称noCleaner
  • noCleanerfalse:创建InstrumentedUnpooledUnsafeDirectByteBuf对象,简称hasCleaner

什么是noCleaner细分场景?

  • hasunsafe
  • 能够调用unSafe.freeMemory(address)去释放内存
  • 能够反射调用private DirectByteBuffer(long addr, int cap)去分配内存

Desktop View UnpooledByteBufAllocator的构造函数

什么是hasCleaner细分场景?

  • hasunsafe
  • 不能反射调用private DirectByteBuffer(long addr, int cap)进行对象创建
  • 能够反射调用DirectByteBuffer(int cap)去分配内存,创建对象
  • 能够使用DirectByteBufferCleanerclean方法回收内存

noCleanerhasCleaner细分场景不同

  • 构造器方式不同
    • noCleaner:反射调用private DirectByteBuffer(long addr, int cap),双参数,类私有,无cleaner
    • hasCleanernew操作调用DirectByteBuffer(int cap),单参数,包私有,有cleaner
  • 两个释放内存方式不同:
    • noCleaner:使用unSafe.freeMemory(address)完成内存释放
    • hasCleaner:使用DirectByteBufferCleanerclean方法回收内存

什么是Cleaner

  • Cleaner用来主动释放内存
    • JDKDirectByteBuffer中提供了Cleaner用来主动释放内存,作用和UnsafefreeMemory方法类似。
  • Cleaner对象的构造时机
    • 在单参数、包私有的DirectByteBuffer(int cap)构造器中完成创建。
  • Cleaner是个虚引用
    • 这个Cleaner属于虚引用类型,DirectByteBuffer创建它的时候,会将自身传入虚引用的构造函数中。当这个DirectByteBuffer不再被引用、即将被回收时,GC会把对应的Cleaner对象挂载到PendingList上。JVM中专门有一条ReferenceHandler线程会死循环扫描这个List,执行ReferencetryHandlePending方法,该方法会调用pending状态下Cleanerclean方法,最终完成对应的直接内存回收操作。

hasCleanerDirectByteBuffer(int cap),单参数,包私有,有cleaner

Desktop View DirectByteBuf的构造函数

Cleaner的性能问题
在运行过程中,Cleaner回收的内存,有一定的时间滞后。 noCleaner无论是在申请内存,还是释放内存,都比使用hasCleaner性能要好一点。 所以,Netty4.1引入了noCleaner策略。

noCleanerDirectByteBuffer构造器,双参数,类私有,无cleanerDirectByteBuffer构造器,

Desktop View DirectByteBuf的构造函数

noCleaner的双参数构造方法DirectByteBuffer(long addr, int cap)没调用cleanerclean方法的。

Desktop View UnpooledUnsafeDirectByteBuf的继承关系

NoCleaner内存分配

1
2
3
4
5
6
7
InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf的内存分配
--> InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf 构造器
--> UnpooledUnsafeNoCleanerDirectByteBuf 构造器
--> UnpooledUnsafeDirectByteBuf 构造器
--> InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf.allocateDirect
--> UnpooledUnsafeNoCleanerDirectByteBuf.allocateDirect
--> PlatformDependent.allocateDirectNoCleaner

UnpooledUnsafeNoCleanerDirectByteBuf内存分配总结

  • 该类继承自UnpooledUnsafeDirectByteBuf
  • 先通过Unsafe分配内存,再构造DirectByteBuffer对象
    • 分配直接内存时,是通过Unsafe.allocateMemory(int capacity)分配到直接内存,拿到直接内存起始地址address,再以address为参数,反射调用DirectByteBuffer的以下构造器创建DirectByteBuffer对象,
    • DirectByteBuffer(long addr, int cap, Object ob)
  • 没有cleaner对象,调用Unsafe去释放

注意,DirectByteBuffer的这个构造器,不会创建cleaner对象,这也就是该类名有NoCleaner关键字的原因。既然没有cleaner,释放内存时也就不能通过cleaner来释放,所以释放内存时,是直接调用Unsafe.freeMemory(long address)来完成释放的。

看完内存分配,再看下NoCleanersetbytes钩子实现。

UnpooledUnsafeNoCleanerDirectByteBufsetbytes钩子实现,继承自UnpooledUnsafeDirectByteBuf,通过unsafe直接对内存进行读写,所以性能上会好一些。

Desktop View UnpooledUnsafeDirectByteBuf setBytes

hasCleaner内存分配

1
2
3
4
5
InstrumentedUnpooledUnsafeDirectByteBuf的内存分配
--> InstrumentedUnpooledUnsafeDirectByteBuf 构造器
--> UnpooledUnsafeDirectByteBuf 构造器
--> UnpooledUnsafeDirectByteBuf.allocateDirect
--> ByteBuffer.allocateDirect

hasCleanersetbytes钩子实现

InstrumentedUnpooledUnsafeDirectByteBufsetbytes钩子实现,继承自UnpooledUnsafeDirectByteBuf,通过unsafe直接对内存进行读写,所以性能上会好一些。

Desktop View UnpooledUnsafeDirectByteBuf setBytes

直接内存与堆内存简单对比

  • 堆缓冲区可以不用池化
    • 堆缓冲区直接将数据存储在JVM堆空间中,它能在没有使用池化的情况下快速的分配和释放。
  • 直接内存不池化会有性能损耗
    • 直接缓冲区是另外一种ByteBuf模式,它将缓冲区分配在堆外内存(非JVM运行时数据区),垃圾收集器不会管理这部分内存。
  • 直接内存的优势
    • 它的优势在于网络数据传输场景下减少了一次内存复制,其劣势在于分配和释放都较为昂贵。

所以,通常在IO通信线程的读写缓冲区使用DirectByteBuf(不过需要池化),后端业务消息的编解码模块使用HeapByteBuf(可以是非池化),可以达到很好的性能效果。

非池化看完了,接下来就是池化的内存。实际上在Netty里更多使用的是池化的内存。

PooledDirectByteBuf池化的直接内存

Netty 4开始,Netty加入了内存池管理,采用内存池管理比普通的new ByteBuf性能提高了数十倍。

Desktop View PooledByteBuf的继承关系

PooledByteBuf的分配

  • 形的部分(JVMPooledByteBuf对象)的分配
  • 神的部分(实际的字节数组内存空间)的分配

形,说的是PooledByteBuf对象记录着池化内存的地址,偏移量这些属性; 神,保存着缓冲区的字节数组,对应直接内存,这一部分就是对应数据所在。

形的分配

通过对象池,获取PooledHeapByteBuf对象,之后做重复利用,充分说明Netty的性能还是比较高的。

PooledHeapByteBuf

由于使用了对象池,每次创建PooledHeapByteBuf的时候,不是直接new,而是从对象池中去获取,然后设置引用计数器、读写index和缓冲区最大容量返回。

1
2
3
4
5
6
7
8
9
10
11
12
static PooledHeapByteBuf newInstance(int maxCapacity) {
    PooledHeapByteBuf buf = RECYCLER.get();
    buf.reuse(maxCapacity);
    return buf;
}

final void reuse(int maxCapacity) {
    maxCapacity(maxCapacity);
    setRefCnt(1);
    setIndex0(0, 0);
    discardMarks();
}

池化的直接内存,套路也是一样的。

PooledDirectByteBuf

PooledDirectByteBuf使用了对象池,用RECYCLER来做回收重复利用。调用memoryget方法获取数据,memory是一个DirectByteBuffer对象。

1
2
3
4
5
6
7
8
9
10
11
12
private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() {
    @Override
    protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
        return new PooledDirectByteBuf(handle, 0);
    }
};

static PooledDirectByteBuf newInstance(int maxCapacity) {
    PooledDirectByteBuf buf = RECYCLER.get();
    buf.reuse(maxCapacity);
    return buf;
}

神的分配(数据内存空间的分配)

内存池的最顶层,可以理解为区,一个大的区,可以细分成很多Chunk块,一个块如果还太大的话,就细分为页。假设1个块16M,一个页512K等。

Pool内存管理组件
Netty4之后加入内存池管理,通过内存池管理比之前ByteBuf的创建性能得到了极大提高。

  • PoolArena竞技区‌
    • 在内存分配中,为了能够集中管理内存的分配及释放,同时提升分配和释放内存的性能,一般都会预先分配一大块连续的内存,不需要重复频繁地进行内存操作,这一大块连续的内存就叫做memory Arena,而PoolArenaNetty的内存池实现类。
  • Chunk块‌
    • Netty中,PoolArena是由多个Chunk组成的,而每个Chunk则由多个Page组成,PoolArenaChunkPage共同完成组织和管理工作。
  • PoolSubpage页‌
    • 当进行小于一个Page的内存分配时,每个Page会被划分为大小相等的内存块,它的大小是根据第一次申请内存分配的内存块大小来决定的。一个Page只能分配与第一次申请的内存块大小相等的内存块,如果想要申请大小不相等的内存块,只能在新的Page上申请内存分配。Page中的存储区域的使用情况是通过一个long类型的数组bitmap来维护的,每一位表示一个区域的占用情况。

PoolThreadCache

Desktop View PoolThreadCache

在每个线程去申请内存的时候,首先会通过ThreadLocal这种方式去获得当前线程的PoolThreadCache对象类型的cache对象, PoolThreadCache一共分为两部分,一个是cache,在申请内存的时候首先会尝试从对象池中获取。

ChunkList

Desktop View ChunkList

一个arena维护着多个ChunkList,每个ChunkList代表着它们内部的Chunk的使用率情况,这些Chunk会进行动态移动,每次完成内存分配后都会重新计算它们的使用率,判定其应当归属的ChunkList,随后将其移动到对应的列表中。每次执行内存分配操作时,会先通过指定算法定位到对应的ChunkList,再从该列表里挑选适配的Chunk完成内存分配。

Chunk

一个Chunk16M,进行一次内存分配,不可能一次将一个Chunk全部分配,于是又将Chunk分割成更小的Page

Desktop View Chunk

一个Chunk会以8K的大小进行划分,分成一个个的Page,到时候分配内存的时候只需要以Page为单位进行内存划分。但是,如果我只需要一个2K大小的内存,那么分配给我一个Page岂不是又造成了浪费,于是又继续将Page进行划分成更小的subpage

形和神的关系:内存池字节缓冲的表示

它们都是PoolChunk分配到的一大段连续内存上的一小段,通过下标来控制访问范围。它们的主要属性在父类PooledByteBuf中,

1
2
3
4
5
// memory,记录的是块的地址
protected T memory;
// offset,记录的是ByteBuf在块上的偏移量
protected int offset;
protected int length;

假设从同一个chunk上分配了两个堆字节缓冲,偏移量分别是(offset=16 bytes,length=16 bytes)(offset=32 bytes,length=16 bytes),那么他们是一大段连续内存上的两个片段,如图所示,

Desktop View Chunk示例

PooledByteBufAllocator

顾名思义,PooledByteBufAllocator就是一个用于分配PooledByteBuf的分配器。

线程的局部变量缓存,每个线程都有这样一个局部变量,ThreadLocal类型,

PooledByteBufAllocator主要属性有三个,

1
2
3
private final PoolArena<byte[]> heapArenas;
private final PoolArena<ByteBuffer> directArenas;
private final PoolThreadLocalCache threadCache;
  • heapArenas是堆内存的表示,每个arena内存以字节数组byte[]表示,注意arena不会直接持有内存,arena所有内存通过PoolChunk持有。
  • directArenas是直接内存的表示,每个arena的内存以DirectByteBuffer表示。
  • threadCacheFastThreadLocal的子类对象。

这三个属性都在创建PooledByteBufAllocator对象时初始化。

看个简单的例子,通过默认的池化分配器,分配一个1024字节的ByteBuf内存,

1
2
3
4
5
public void testUnpooledDirectByteBuf() {
    PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
    ByteBuf buffer = allocator.buffer(1024);
    print("allocate ByteBuf(9, 100)", buffer);
}
1
2
3
4
5
6
7
PooledDirectBuffer的调用路径
AbstractByteBufAllocator.directBuffer
PooledByteBufAllocator.newDirectBuffer
PoolArena.allocate(PoolThreadCache cache, PooledByteBuf buf, final int reqCapacity)
PoolArena.allocateNormal(PooledByteBuf buf, int reqCapacity, int normCapacity)
PoolArena DirectArena.newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize)
PoolArena DirectArena.allocate(int capacity)

Desktop View AbstractByteBufAllocator的directBuffer方法

Desktop View PooledByteBufAllocator newDirectBuffer

形的分配:newByteBufJava对象的对象池RECYCLER
神的分配:allocatePoolArena直接内存池)

神的分配 PoolArena.allocate

Desktop View PoolArena.allocate

这部分很复杂,分配一块大内存,计算它的块和页,找到应该对应到哪一页,把页的地址拿出来,给到ByteBuf就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
    final int normCapacity = normalizeCapacity(reqCapacity);
    if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
        int tableIdx;
        PoolSubpage<T>[] table;
        boolean tiny = isTiny(normCapacity);
        if (tiny) { // < 512
            if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                // was able to allocate out of the cache so move on
                return;
            }
            tableIdx = tinyIdx(normCapacity);
            table = tinySubpagePools;
        } else {
            if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                // was able to allocate out of the cache so move on
                return;
            }
            tableIdx = smallIdx(normCapacity);
            table = smallSubpagePools;
        }

        final PoolSubpage<T> head = table[tableIdx];

        /**
         * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
         * {@link PoolChunk#free(long)} may modify the doubly linked list as well.
         */
        synchronized (head) {
            final PoolSubpage<T> s = head.next;
            if (s != head) {
                assert s.doNotDestroy && s.elemSize == normCapacity;
                long handle = s.allocate();
                assert handle >= 0;
                s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
                incTinySmallAllocation(tiny);
                return;
            }
        }
        synchronized (this) {
            allocateNormal(buf, reqCapacity, normCapacity);
        }

        incTinySmallAllocation(tiny);
        return;
    }
    if (normCapacity <= chunkSize) {
        if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
            // was able to allocate out of the cache so move on
            return;
        }
        synchronized (this) {
            allocateNormal(buf, reqCapacity, normCapacity);
            ++allocationsNormal;
        }
    } else {
        // Huge allocations are never served via the cache so just call allocateHuge
        allocateHuge(buf, reqCapacity);
    }
}

// Method must be called inside synchronized(this) { ... } block
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
        q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
        q075.allocate(buf, reqCapacity, normCapacity)) {
        return;
    }

    // Add a new chunk.
    PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
    long handle = c.allocate(normCapacity);
    assert handle > 0;
    c.initBuf(buf, handle, reqCapacity);
    qInit.add(c);
}

@Override
protected PoolChunk<ByteBuffer> newChunk(int pageSize, int maxOrder,
        int pageShifts, int chunkSize) {
    if (directMemoryCacheAlignment == 0) {
        return new PoolChunk<ByteBuffer>(this,
                allocateDirect(chunkSize), pageSize, maxOrder,
                pageShifts, chunkSize, 0);
    }
    final ByteBuffer memory = allocateDirect(chunkSize
            + directMemoryCacheAlignment);
    return new PoolChunk<ByteBuffer>(this, memory, pageSize,
            maxOrder, pageShifts, chunkSize,
            offsetCacheLine(memory));
}

@Override
protected PoolChunk<ByteBuffer> newUnpooledChunk(int capacity) {
    if (directMemoryCacheAlignment == 0) {
        return new PoolChunk<ByteBuffer>(this,
                allocateDirect(capacity), capacity, 0);
    }
    final ByteBuffer memory = allocateDirect(capacity
            + directMemoryCacheAlignment);
    return new PoolChunk<ByteBuffer>(this, memory, capacity,
            offsetCacheLine(memory));
}

private static ByteBuffer allocateDirect(int capacity) {
    return PlatformDependent.useDirectBufferNoCleaner() ?
            PlatformDependent.allocateDirectNoCleaner(capacity) : ByteBuffer.allocateDirect(capacity);
}

内存池的内存分配工作,实际上发生在创建块的时候,其他地方只需要做内存地址的计算。

形的分配 PoolArena DirectArena.newByteBuf

使用了Java对象的对象池RECYCLER

Desktop View PoolArena.newByteBuf

Desktop View PooledUnsafeHeapByteBuf.newUnsafeInstance

本文由作者按照 CC BY 4.0 进行授权

© ManShouyuan. 保留部分权利。

本站总访问量 本站访客数人次

🚩🚩🚩🚩🚩🚩