Netty内存管理概述

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/TheLudlows/article/details/86144788

1. 内存管理概述

内存管理的主要目的合理分配内存,减少内存碎片,及时回收资源,提高内存的使用效率。

从操作系统层面来说,各个软件在运行时向操作系统请求对计算机内存资源进行快速的分配,并且在适当的时候释放和回收内存资源。常见的一些算法有slab,buddy,jemalloc等经典算法。

从Netty层面来说,其实质就是先分配一块大内存,然后在内存的分配和回收过程中,使用一些数据结构记录内存使用状态,如果有新的分配请求,根据这些状态信息寻找最合适的位置返回并更新数据结构;内存使用完释放后,同步修改数据结构。
Netty的内存管理分为有缓冲池和无缓冲池的,有缓冲池的内存分配器会在内存回收时,将信息记录在缓冲池中,下次如果有合适的分配请求则直接从缓冲池中复用。在实践中,由于多线程网络请求处理很快,分配和回收在有缓冲池的(pooled)效率更高。

2. 分配算法概述

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

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

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

  1. 内存分配的最小单位为16B。
  2. 小于512B的请求为Tiny,小于8KB(PageSize)的请求为Small,小于等于16MB(Chunk Size)的请求为Normal,大于16MB(Chun kSize)的请求为Huge。
  3. 小于512B的请求以16B为起点每次增加16B;大于等于512B的请求则每次加倍。

在jemalloc中建议为4MB,Netty默认使用16MB。为了进一步提高内存利用率并减少内部碎片,需要继续将Chunk切分为小的块Page。一般将Chunk切分为2048块,Page的大小为:16MB/2048=8KB。

为了分配内存块保持连续和减少内存碎片,因此jemalloc使用Buddy内存分配算法。
Netty采用完全二叉树进行管理,树中每个叶子节点表示一个Page,即树高为12。具有相同父节点的叶子节点称为buddy关系,buddy之间自底向上链接为二叉树,直到根节点。
netty
举个例子:8KB、16KB、8KB为例分析分配过程(每个Page大小8KB):

  1. 8KB:需要一个Page,第11层满足要求,故分配2048节点即Page0;
  2. 16KB:需要两个Page,故需要在第10层进行分配,而1024的子节点2048已分配,从左到右找到满足要求的1025节点,故分配节点1025即Page2和Page3;
  3. 8KB:需要一个Page,第11层满足要求,2048已分配,从左到右找到2049节点即Page1进行分配。

分配结束后,已分配连续的Page0-Page3,这样的连续内存块,大大减少内部碎片并提高内存使用率

Netty中每个Page的默认大小为8KB,在实际使用中,很多业务需要分配更小的内存块比如16B、32B、64B等。为了应对这种需求,需要进一步切分Page成更小的SubPage。SubPage是jemalloc中内存分配的最小单位,不能再进行切分。SubPage切分的单位并不固定,以第一次请求分配的大小为单位(最小切分单位为16B)。比如,第一次请求分配32B,则Page按照32B均等切分为256块;

3. ByteBufAllocator

此接口来进行实际的内存分配,默认使用的是ByteBufAllocator.DEFAULT,初始化时会根据配置和平台进行赋值。io.netty.allocator.type可以设置为unpooled和pooled指定是否需要缓冲池,如果不设置则会根据平台判断。逻辑如下:

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;
}

AbstractByteBufAllocator继承此接口,实现了一些通用的逻辑,比如最基本的buffer方法。
netty

@Override
public ByteBuf buffer() {
    if (directByDefault) {
        return directBuffer();
    }
    return heapBuffer();
}

根据directByDefault属性来判断创建不同类型的buffer,至于directByDefault属性的赋值,在此抽象类中并没有赋值操作。
经过一系列的重载之后调用了如下两个抽象方法:

protected abstract ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity);
protected abstract ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity);

它们的实现在UnPooledByteBufAllocator或者PooledByteBufAllocator中,我们先看前者的实现逻辑。

3.1 UnPooledByteBufAllocator

该分配器分配的Bytebuf底层为不使用对象池技术字节数组。至于直接内存还是堆内存取决于参数,至于是否通过Unsafe分配取决于配置以及平台,这里多说一点,一般创建对象通过new关键字,即三条指令创建对象,反射也一样。不清楚的可以看下:Java中的指针:Unsafe类
,其实Unsafe/safe分配器对于ByteBuf的创建没有多大的差异,只是写入数据的效率Unsafe是通过内存地址直接写入,会更快一些。

从它的构造函数入手:

public UnpooledByteBufAllocator(boolean preferDirect, boolean disableLeakDetector, boolean tryNoCleaner) {
    super(preferDirect);
    this.disableLeakDetector = disableLeakDetector;
    noCleaner = tryNoCleaner && PlatformDependent.hasUnsafe()
            && PlatformDependent.hasDirectBufferNoCleanerConstructor();
}

第一个参数表示是否为直接内存,第二个为是否开启内存泄漏检测,第三个参数由平台决定的,表明Netty是否参与直接内存的回收。
其中还调用了父类的构造方法:

protected AbstractByteBufAllocator(boolean preferDirect) {
    directByDefault = preferDirect && PlatformDependent.hasUnsafe();
    emptyBuf = new EmptyByteBuf(this);
}

主要是对前面刚提到的directByDefault属性进行赋值。UnPooledByteBufAllocator对newDirectBuffer和newHeapBuffer都进行了实现。

3.2 PooledByteBufAllocator

Netty实际使用内存分配器会根据配置采用PooledByteBufAllocator.DEFAULT,所有事件循环线程使用的是一个分配器实例。PooledByteBufAllocator将内存分为PoolArena,PoolChunk和PoolPage,Chunk中包含多个内存页,Arena包含3个Chunk。
先从重要的常量看起。

// 默认堆内存类型PoolArena个数
private static final int DEFAULT_NUM_HEAP_ARENA;
// 默认直接内存类型PoolArena个数
private static final int DEFAULT_NUM_DIRECT_ARENA;
// 默认页大小
private static final int DEFAULT_PAGE_SIZE;
// 每个chunk中的page是用平衡二叉树映射管理每个PoolSubpage是否被分配
// maxOrder为树的深度,深度为maxOrder层的节点数量为1 << maxOrder,maxOrder =< 11
private static final int DEFAULT_MAX_ORDER; 
 //默认的tiny cache 的大小 512
private static final int DEFAULT_TINY_CACHE_SIZE;
 //默认的small cache的大小 256
private static final int DEFAULT_SMALL_CACHE_SIZE;
//默认的normal cache的大小 64
private static final int DEFAULT_NORMAL_CACHE_SIZE;
private static final int MIN_PAGE_SIZE = 4096;
//最大Chunk的大小,默认等于2的30次方 即1G
private static final int MAX_CHUNK_SIZE = (int) (((long) Integer.MAX_VALUE + 1) / 2);

在静态代码块中对这些常量进行初始化操作。PooledByteBufAllocator内部有两个重要数组HeapArena和DirectArena,用来记录堆内存和直接内存当前的使用状态。PoolArena都实现了PoolArenaMetric接口,用于测量内存使用状况。数组的长度即为上面的定义的常量。除此之外,还有一个重要的对象PoolThreadLocalCache,其继承了FastThreadLocal,用于线程的本地缓存,在内存管理中,线程本地内存缓区的信息会保存在PoolThreadCache对象中。PooledByteBufAllocator覆盖的newHeapBuffer和newDirectBuffer用来分配内存。构造函数中对成员变量进行过初始化操作。

public PooledByteBufAllocator(boolean preferDirect, int nHeapArena, int nDirectArena, int pageSize, int maxOrder,
                              int tinyCacheSize, int smallCacheSize, int normalCacheSize,
                              boolean useCacheForAllThreads, int directMemoryCacheAlignment) {
    super(preferDirect);
    // PoolThreadLocalCache继承自FastThreadLocal,用来保存PoolThreadCache
    threadCache = new PoolThreadLocalCache(useCacheForAllThreads);
    this.tinyCacheSize = tinyCacheSize;
    this.smallCacheSize = smallCacheSize;
    this.normalCacheSize = normalCacheSize;
    // chunkSize = DEFAULT_PAGE_SIZE(8KB) << DEFAULT_MAX_ORDER(11)
    chunkSize = validateAndCalculateChunkSize(pageSize, maxOrder);
    // 检查pageSize是否大于4K且为2的幂次方,如果不是则抛异常,返回的值为pageSize二进制的尾部0的个数 
    // pageSize如果为是8192时它的二进制表示是10000000000000,那么这个pageShifts就是13
    int pageShifts = validateAndCalculatePageShifts(pageSize);
    // 省略HeadArena的构造... 
    if (nDirectArena > 0) {
        // 创建DirectArena数组
        directArenas = newArenaArray(nDirectArena);
        List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(directArenas.length);  
        for (int i = 0; i < directArenas.length; i ++) {
            PoolArena.DirectArena arena = new PoolArena.DirectArena(
                    this, pageSize, maxOrder, pageShifts, chunkSize, directMemoryCacheAlignment);
            // 为每一个元素赋值
            directArenas[i] = arena;
            // arena实现了PoolArenaMetric,也添加到指标列表中
            metrics.add(arena);
        }
        // 返回只读的列表
        directArenaMetrics = Collections.unmodifiableList(metrics);
    }
    // PooledByteBufAllocator的测量指标
    metric = new PooledByteBufAllocatorMetric(this);
}

构造函数省略了很大一部分判断代码,看起卡比较直观易懂。其中有一个比较有意思是对pageShifts的判断获取。代码如下:

private static int validateAndCalculatePageShifts(int pageSize) {
    if (pageSize < MIN_PAGE_SIZE) {
        throw new IllegalArgumentException("pageSize: " + pageSize + " (expected: " + MIN_PAGE_SIZE + ")");
    }
    // 判断是否为2的幂次方,这里减号的优先级高
    if ((pageSize & pageSize - 1) != 0) {
        throw new IllegalArgumentException("pageSize: " + pageSize + " (expected: power of 2)");
    }
    
    // 能走到这里,说明pageSize是2的幂次方,numberOfLeadingZeros是返回最高位之前的0的个数
    return Integer.SIZE - 1 - Integer.numberOfLeadingZeros(pageSize);
}

下面继续分析对newHeadpBuffer的实现:

@Override
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
    // 获取到PoolThreadLocalCache
    PoolThreadCache cache = threadCache.get();
    PoolArena<byte[]> heapArena = cache.heapArena;

    final ByteBuf buf;
    if (heapArena != null) {
        // 真正的分配内存
        buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
    } else {
        buf = PlatformDependent.hasUnsafe() ?
                new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
                new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
    }
    
    return toLeakAwareBuffer(buf);
}

可以看出真正的分配内存逻辑以及池化的操作是在PoolArena中,本文主要对Netty内存分配器ByteBufAllocator进行介绍,以及对Jemelloc思想的描述。后面会对Netty内存分配的池化、分配与释放、内存泄漏检测以及缓存的使用进行深入的分析。

展开阅读全文

没有更多推荐了,返回首页