美文网首页
Netty内存分配原理

Netty内存分配原理

作者: 爱健身的兔子 | 来源:发表于2020-11-02 11:33 被阅读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 中的数据容器分类

    按照底层存储空间划分:

    • 堆缓冲区:HeapBuffer;
    • 直接缓冲区:DirectBuffer。

    按照是否池化划分:

    • 池化:PooledBuffer;
    • 非池化:UnPooledBuffer。

    默认使用 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:一个 Chunk 的大小是 16M,Chunk 是 Netty 对操作系统进行内存申请的单位,后续所有的内存分配都是在 Chunk 里面进行操作。
    • Page:Chunk 内部以 Page 为单位分配内存,一个 Page 大小为 8K。当我们需要 16K 的空间时,Netty 就会从一个 Chunk 中找到两个 Page 进行分配。
    • Subpage 和 element:element 是比 Page 更小的单位,当我们申请小于 8K 的内存时,Netty 会以 element 为单位进行内存分配。element 没有固定大小,具体由用户的需求决定。Netty 通过 Subpage 管理 element,Subpage 是由 Page 转变过来的。当我们需要 1K 的空间时,Netty 会把一个 Page 变成 Subpage,然后把 Subpage 分成 8 个 1K 的 element 进行分配。

    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/

    内存分配策略 - 百度文库

    相关文章

      网友评论

          本文标题:Netty内存分配原理

          本文链接:https://www.haomeiwen.com/subject/iazpvktx.html