Netty内存分配原理

2020-11-02  本文已影响0人  爱健身的兔子

1 java NIO的ByteBuffer

Bytebuffer分为两种:HeapByteBuffer(堆内内存),DirectByteBuffer(堆外内存)。

HeapByteBuffer,在jvm堆上面的一个buffer,底层的本质是一个数组。有JVM垃圾回收期创建和回收。

DirectByteBuffer,底层的数据其实是维护在操作系统的内存中,而不是jvm里,DirectByteBuffer里维护了一个引用地址指向了堆外内存,从而操作数据。

HeapByteBuffer缺点:由于内容维护在jvm里,对IO操作时,会在内核空间与用户空间中拷贝数据。

HeapByteBuffer优点:由于内容维护在jvm里,所以把内容写进buffer里速度会快些;并且,可以更容易回收。

DirectByteBuffer缺点:堆外内存的分配效率比堆内内存的低,而且访问速度也比堆内存慢。

DirectByteBuffer优点:省去了数据拷贝到JVM中的步骤,实现zero copy(零拷贝)。

2 DirectByteBuffer的分配与回收

ByteBuffer堆外内存的分配

    • 如果空间不足,会调用System.gc()尝试释放内存,然后再进行判断,如果还是没有足够的空间,抛出OOM。
    • 确定有足够的空间后,使用sun.misc.Unsafe#allocateMemory申请内存;
    • 最后,DirectByteBuffer使用Cleaner机制进行空间回收

    说明:

    1. sun.misc.Unsafe.allocateMemory这个函数是通过JNI调用C的malloc来申请内存;
    2. 申请内存时,可以通过-XX:+PageAlignDirectMemory:指定申请的内存是否需要按页对齐,默认不对齐;
    3. 默认堆外内存大小为可用的最大Java堆大小(可以通过-XX:MaxDirectMemorySize设置)

ByteBuffer堆外内存的回收

堆内的ByteBuffer对象本身会被垃圾回收正常的处理,但是堆外的内存就不会被GC回收了,所以需要一个机制,在DirectByteBuffer回收时,同时回收其堆外申请的内存。

Java中可选的特性有finalize函数(对象被gc回收前的准备工作),但是finalize机制是Java官方不推荐的,官方推荐的做法是使用虚引用来处理对象被回收时的后续处理工作。同时Java提供了Cleaner类来简化这个实现,Cleaner是PhantomReference的子类,可以在PhantomReference被加入ReferenceQueue时触发对应的Runnable回调。

DirectByteBuffer就是使用Cleaner机制来实现本身被GC时,回收堆外内存的能力。

3 Netty 中的数据容器分类

按照底层存储空间划分:

按照是否池化划分:

默认使用 PoolDirectByteBuf 类型的内存, 这些内存主要由 PoolArea 管理。另外 Netty 并不是直接对外暴露这些 API,提供了 Unsafe 类作为出口暴露数据分配的相关操作。

4 Netty 中的PoolBuffer的内存分配

Netty 采用了 jemalloc 的思想,这是 FreeBSD 实现的一种并发 malloc 的算法。jemalloc 依赖多个 Arena(分配器) 来分配内存,运行中的应用都有固定数量的多个 Arena,默认的数量与处理器的个数有关。系统中有多个 Arena 的原因是由于各个线程进行内存分配时竞争不可避免,这可能会极大的影响内存分配的效率,为了缓解高并发时的线程竞争,Netty 允许使用者创建多个分配器(Arena)来分离锁,提高内存分配效率。

线程首次分配/回收内存时,首先会为其分配一个固定的 Arena。线程选择 Arena 时使用 round-robin 的方式,也就是顺序轮流选取。

每个线程各种保存 Arena 和缓存池信息,这样可以减少竞争并提高访问效率。Arena 将内存分为很多 Chunk 进行管理,Chunk 内部保存 Page,以页为单位申请。申请内存分配时,会将分配的规格分为几类:TINY,SAMLL,NORMAL 和 HUGE,分别对应不同的范围,处理过程也不相同。

4

tiny 代表了大小在 0-512B 的内存块;

small 代表了大小在 512B-8K 的内存块;

normal 代表了大小在 8K-16M 的内存块;

huge 代表了大于 16M 的内存块。

每个块里面又定义了更细粒度的单位来分配数据:

Chunk 中的内存分配

线程分配内存主要从两个地方分配: PoolThreadCache 和 Arena。其中 PoolThreadCache 线程独享, Arena 为几个线程共享。

5

初次申请内存的时候,Netty 会从一整块内存(Chunk)中分出一部分来给用户使用,这部分工作是由 Arena 来完成。而当用户使用完毕释放内存的时候,这些被分出来的内存会按不同规格大小放在 PoolThreadCache 中缓存起来。当下次要申请内存的时候,就会先从 PoolThreadCache 中找。

Chunk、Page、Subpage 和 element 都是 Arena 中的概念,Arena 的工作就是从一整块内存中分出合适大小的内存块。Arena 中最大的内存单位是 Chunk,这是 Netty 向操作系统申请内存的单位。而一块 Chunk(16M) 申请下来之后,内部会被分成 2048 个 Page(8K),当用户向 Netty 申请超过 8K 内存的时候,Netty 会以 Page 的形式分配内存。

Chunk 内部通过伙伴算法管理 Page,具体实现为一棵完全平衡二叉树:

6

二叉树中所有子节点管理的内存也属于其父节点。当我们要申请大小为 16K 的内存时,我们会从根节点开始不断寻找可用的节点,一直到第 10 层。那么如何判断一个节点是否可用呢?Netty 会在每个节点内部保存一个值,这个值代表这个节点之下的第几层还存在未分配的节点。比如第 9 层的节点的值如果为 9,就代表这个节点本身到下面所有的子节点都未分配;如果第 9 层的节点的值为 10,代表它本身不可被分配,但第 10 层有子节点可以被分配;如果第 9 层的节点的值为 12,此时可分配节点的深度大于了总深度,代表这个节点及其下面的所有子节点都不可被分配。下图描述了分配的过程:

7

对于小内存(小于4096)的分配还会将 Page 细化成更小的单位 Subpage。Subpage 按大小分有两大类:

  1. Tiny:小于 512 的情况,最小空间为 16,对齐大小为 16,区间为[16,512),所以共有 32 种情况。
  2. Small:大于等于 512 的情况,总共有四种,512,1024,2048,4096。

PoolSubpage 中直接采用位图管理空闲空间(因为不存在申请 k 个连续的空间),所以申请释放非常简单。

DirectByteBuffer堆外内存申请、回收_赶路人儿-CSDN博客

Netty 中的内存分配浅析 - rickiyang - 博客园

https://www.douban.com/note/355211972/

内存分配策略 - 百度文库

上一篇下一篇

猜你喜欢

热点阅读