netty中的bytebuf(脱离JVM的垃圾收集实现手动回收)及引用和内存泄漏

本文探讨了Netty中ByteBuf的两种类型:HeapByteBuf和DirectByteBuf,及其内存管理策略。分析了不同引用类型如何影响内存回收,并讨论了内存池技术在减少内存泄漏中的作用。

从内存分配的角度看,ByteBuf可以分为两类:

1、堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被JVM自动回收;缺点就是如果进行Socket的IO读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降

2、直接内存(DirectByteBuf) 字节缓冲区:非堆内存,它在对外进行内存分配,相比于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从Socket Channel中读取时,由于少一次内存复制,速度比堆内存快

Netty的最佳实践是在I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf,这样组合可以达到性能最优。

 

上面的最佳实践是Netty权威指南上写的,下面结合jvm中的引用,堆那内存堆外内存和内存池技术进行大概的分析。

java中的引用有强引用(StrongReference) 软引用(SoftReference) 弱引用(WeakReference) 和虚引用 (PhantomReference)

⑴强引用(StrongReference)

    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

⑵软引用(SoftReference)

    如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

    软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

⑶弱引用(WeakReference)

    弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

    弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

⑷虚引用(PhantomReference)

    “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

    虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

 

http://www.importnew.com/22205.html

Netty之有效规避内存泄漏

 

Heap buffers need to be copied to the direct memory before being handled by the kernel, when you're performing I/O operations. When you use direct buffers you save that copy operation and that's the main advantage of using direct buffers. A drawback is that direct memory allocation is reasonably more expensive then allocations from the java heap, so Netty introduced the pooling concept.

当进行io操作时,内核操作之前,堆内的bytebuffer需要拷贝到直接内存中。当使用堆外内存时,就省了拷贝的步骤,并且这是最主要的优点。缺点就是堆外内存的分配比较昂贵,所以netty中有了一个池的概念。

Pooling objects in Java is a polemic topic, but the Netty choice for doing so seems to have paid off and the Twitter article you cited shows some evidence of that. For the particular case of allocating buffers, when the size of the buffer is large, you can see that it really brings a benefit both in the direct and heap buffer cases.

池对象在java中一直是一个有争议的主题。netty选择这样做是有一定的道理的,twitter也有证明显示这样有好处。

Now for pooling, the GC doesn't reclaim the buffer when they are pooled, because either your application has one or several references to it, while you're using the buffer; or Netty's pool has a reference to it, when it has just been allocated and has not yet been given to your application or after your application used it and gave it back to the pool.

Leaks will happen when your application, after having used a buffer and not keeping any further reference to it, doesn't call release(), what actually means put it back into the pool, if you don't have any further reference to it. In such case, the buffer will eventually be garbage collected, but Netty's pool won't know about it. The pool will then grow believing that you're using more and more buffers that are never returned to the pool. That will probably generate a memory leak because, even if the buffers themselves are garbage collected, the internal data structures used to store the pool will not.

GC不会回收池化的buffer,要么因为你的应用在引用它,要么因为它刚被分配时还没让你的应用使用的时候或者你的应用使用完了将它返回时,netty对它有一个引用。

当你用过一个buffer却没有release(实际上就是将它返回到池里),这个buffer会被垃圾回收器回收,但是,它对应的池里的底层数据结构依然被占用,因为netty是靠引用计数来释放那部分数据(以便让其他的操作使用),这就是内存泄露的原因。

In the Sun JDK, a java.nio.DirectByteBuffer—created by ByteBuffer#allocateDirect(int)—has a field of type sun.misc.Cleaner, which extends java.lang.ref.PhantomReference.

When this Cleaner (remember, a subtype of PhantomReference) gets collected and is about to move into the associated ReferenceQueue, the collection-related thread running through the nested type ReferenceHandler has a special case treatment of Cleaner instances: it downcasts and calls on Cleaner#clean(), which eventually makes its way back to calling on DirectByteBuffer$Deallocator#run(), which in turn calls on Unsafe#freeMemory(long). 

在sun jdk中,DirectByteBuffer包含一个属性cleaner,是PhantomReference的子类,也就是虚引用,当这个buffer被回收时,它会被添加到cleaner的referenceQueue中,最终会被调用clean方法,归还堆外内存。

 

在C时代,我们喜欢让malloc和free成对出现,而在Netty里,因为Handler链的存在,ByteBuf经常要传递到下一个Hanlder去而不复还,所以规则变成了谁是最后使用者,谁负责释放。

另外,更要注意的是各种异常情况,ByteBuf没有成功传递到下一个Hanlder,还在自己地界里的话,一定要进行释放。

InBound Message

在AbstractNioByteChannel.NioByteUnsafe.read() 处创建了ByteBuf并调用 pipeline.fireChannelRead(byteBuf) 送入Handler链。

根据上面的谁最后谁负责原则,每个Handler对消息可能有三种处理方式

  • 对原消息不做处理,调用 ctx.fireChannelRead(msg)把原消息往下传,那不用做什么释放。

  • 将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉。

  • 如果已经不再调用ctx.fireChannelRead(msg)传递任何消息,那更要把原消息release掉。

假设每一个Handler都把消息往下传,Handler并也不知道谁是启动Netty时所设定的Handler链的最后一员,所以Netty在Handler链的最末补了一个TailHandler,如果此时消息仍然是ReferenceCounted类型就会被release掉。

OutBound Message

要发送的消息由应用所创建,并调用 ctx.writeAndFlush(msg) 进入Handler链。在每个Handler中的处理类似InBound Message,最后消息会来到HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。

异常发生时的释放

多层的异常处理机制,有些异常处理的地方不一定准确知道ByteBuf之前释放了没有,可以在释放前加上引用计数大于0的判断避免释放失败;

有时候不清楚ByteBuf被引用了多少次,但又必须在此进行彻底的释放,可以循环调用reelase()直到返回true。

 

ByteBuf.duplicate(), ByteBuf.slice() and ByteBuf.order(ByteOrder) create a derived buffer which shares the memory region of the parent buffer. A derived buffer does not have its own reference count but shares the reference count of the parent buffer.

这三个方法返回的ByteBuf和父buf使用同一个内存区域,所以reference count也是一样的

In contrast, ByteBuf.copy() and ByteBuf.readBytes(int) are not derived buffers. The returned ByteBuf is allocated will need to be released.

相反,copy方法和readBytes方法返回的是自己的内存区域,自己处理自己的就行

Sometimes, a ByteBuf is contained by a buffer holder, such as DatagramPacketHttpContent, and WebSocketframe. Those types extend a common interface called ByteBufHolder.

A buffer holder shares the reference count of the buffer it contains, just like a derived buffer.

注意有的ByteBufHolder和derived buffer(衍生buf)一样

转载于:https://my.oschina.net/wuxiaofei/blog/2056309

<think>我们正在讨论Netty中的ByteBufAllocator内存分配策略。根据引用内容,ByteBufAllocator是Netty中内存分配的顶层抽象,负责分配所有ByteBuf类型的内存。它提供了多种分配方法,如buffer()、ioBuffer()、heapBuffer()、directBuffer()compositeBuffer()等,用于分配不同类型的缓冲区。 引用[1]指出,ByteBufAllocator接口的主要方法包括: - buffer(): 自动判断分配堆内或堆外内存 - ioBuffer(): 尽可能分配堆外直接内存(适合I/O),如果不支持则分配堆内内存 - heapBuffer(): 分配堆内内存 - directBuffer(): 分配堆外内存 - compositeBuffer(): 分配组合缓冲区(CompositeByteBuf引用[2][3]则进一步说明了两种具体的实现: - PooledByteBufAllocator: 使用预先分配好的内存池来分配数据,提高性能并减少内存碎片。 - UnpooledByteBufAllocator: 每次分配都直接从操作系统申请内存,不进行池化管理。 此外,引用[3]还解释了堆内存(Heap)堆外内存(Direct)的区别: - Heap: 在JVM堆上分配,受JVM管理,会被垃圾回收,无需手动释放。 - Direct: 在堆外分配,不受JVM管理,需要手动释放,否则会导致内存泄漏。 现在,我们需要详细解释NettyByteBufAllocator如何实现不同的内存分配策略。 实现方式: 1. 抽象类AbstractByteBufAllocator实现ByteBufAllocator接口,它定义了分配内存的基本框架,并提供了两种子类实现:PooledByteBufAllocatorUnpooledByteBufAllocator。 2. 在AbstractByteBufAllocator中,分配内存的流程是统一的,但具体的分配动作(如创建堆内/堆外内存)由子类实现。 3. 对于堆内内存(Heap Buffer)堆外内存(Direct Buffer)的分配,分别由子类实现的方法newHeapBuffernewDirectBuffer完成。 下面我们详细说明: 一、内存分配策略的入口方法 以buffer()方法为例,其默认行为是: - 如果平台支持unsafe,则优先分配直接内存(即堆外内存),否则分配堆内存。 - 具体实现中,会调用heapBuffer()或directBuffer()方法。 二、具体分配策略的实现 1. 堆内内存分配(Heap Buffer): 在AbstractByteBufAllocator中,heapBuffer()方法会调用newHeapBuffer方法,该方法由子类实现。 例如,在UnpooledByteBufAllocator中,newHeapBuffer方法会创建一个UnpooledHeapByteBuf对象,该对象内部使用一个byte数组(在JVM堆上)作为存储。 2. 堆外内存分配(Direct Buffer): directBuffer()方法会调用newDirectBuffer方法,该方法同样由子类实现。 在UnpooledByteBufAllocator中,newDirectBuffer方法会创建一个UnpooledDirectByteBuf对象,该对象内部使用ByteBuffer.allocateDirect()分配的堆外内存。 在PooledByteBufAllocator中,则从预先分配的内存池中获取一块内存。 3. 内存池化(Pooled)与非池化(Unpooled): - 池化分配器(PooledByteBufAllocator): 通过预先分配一大块内存(称为Arena),然后将其划分为不同大小的块(Chunk),再进一步划分为更小的页(Page)。当需要分配内存时,从池中找到合适大小的块,避免频繁向操作系统申请内存。 这种方式减少了内存分配回收的开销,提高了性能,尤其在高并发场景下。 - 非池化分配器(UnpooledByteBufAllocator): 每次分配都直接调用底层API创建新的内存块(对于堆内存,使用new byte[capacity];对于堆外内存,使用ByteBuffer.allocateDirect(capacity))。 这种方式简单,但频繁分配释放可能带来性能开销。 三、组合缓冲区(CompositeByteBuf) compositeBuffer()方法用于创建组合缓冲区,它可以将多个ByteBuf组合成一个逻辑上的整体,而不需要实际复制数据,实现零拷贝。 组合缓冲区的实现同样有池化非池化之分,但底层还是由具体的分配器决定。 四、内存分配器的选择 在Netty中,默认的内存分配器是池化的直接内存分配器(PooledByteBufAllocator.DEFAULT),因为直接内存避免了在I/O操作时的一次内存拷贝(从堆内拷贝到堆外),并且池化提高了性能。 我们可以通过Bootstrap配置来指定分配器: ```java bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); ``` 五、示例代码:使用不同的分配策略 1. 获取默认的分配器(通常为池化分配器): 在ChannelHandlerContext中,可以通过ctx.alloc()获取到分配器。 2. 手动分配一个堆内缓冲区: ```java ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(1024); ``` 3. 手动分配一个堆外缓冲区: ```java ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(1024); ``` 六、内存分配策略的源码分析(简要) 以PooledByteBufAllocator为例,其分配直接内存的流程如下: - 首先,从线程局部缓存(ThreadLocalCache)中获取一个内存池(PoolArena)。 - 然后,在PoolArena中根据所需大小进行分配: * 如果是小块内存(小于页大小),则从缓存的小块内存队列中分配; * 如果是大块内存,则分配一个连续的块(可能涉及多个页)。 在UnpooledByteBufAllocator中,分配堆内内存就是创建一个新的byte数组,而分配堆外内存则是通过ByteBuffer.allocateDirect(capacity)实现。 七、总结 NettyByteBufAllocator通过抽象接口具体实现(池化/非池化)提供了灵活的内存分配策略。用户可以根据需要选择堆内或堆外内存,以及是否使用内存池。池化分配器通过重用内存块提高了性能,而非池化分配器则简化了内存管理。组合缓冲区的支持则进一步优化了数据零拷贝。 根据以上内容,我们回答用户的问题:NettyByteBufAllocator通过抽象类AbstractByteBufAllocator两个具体实现类(PooledByteBufAllocatorUnpooledByteBufAllocator)来实现不同的内存分配策略。对于堆内堆外内存的分配,分别由子类实现的newHeapBuffernewDirectBuffer方法完成。池化分配器通过内存池管理机制重用内存,而非池化分配器每次分配都直接向操作系统申请。 最后,我们生成相关问题。</think>### Netty 中 `ByteBufAllocator` 的内存分配策略实现方式 Netty 的 `ByteBufAllocator` 通过分层设计策略模式实现不同内存分配策略,核心机制如下: --- #### 一、内存分配策略的层级架构 ```mermaid classDiagram ByteBufAllocator <|-- AbstractByteBufAllocator AbstractByteBufAllocator <|-- PooledByteBufAllocator AbstractByteBufAllocator <|-- UnpooledByteBufAllocator class ByteBufAllocator{ <<interface>> +buffer() ByteBuf +ioBuffer() ByteBuf +heapBuffer() ByteBuf +directBuffer() ByteBuf } class AbstractByteBufAllocator{ #newHeapBuffer() ByteBuf #newDirectBuffer() ByteBuf } class PooledByteBufAllocator{ -PoolArena[] heapArenas -PoolArena[] directArenas } class UnpooledByteBufAllocator{ -boolean disableLeakDetector } ``` --- #### 二、策略一:池化 vs 非池化 | 分配器类型 | 实现原理 | 适用场景 | |--------------------------|--------------------------------------------------------------------------|-------------------------| | **PooledByteBufAllocator** | 1. 预分配内存池(Arena)<br>2. 划分固定大小内存块(Chunk)<br>3. 通过 `PoolThreadCache` 实现线程局部缓存[^2] | 高并发、低延迟场景 | | **UnpooledByteBufAllocator** | 1. 每次直接从操作系统分配<br>2. 堆内存:`new byte[capacity]`<br>3. 堆外内存:`ByteBuffer.allocateDirect()`[^3] | 简单应用、低内存消耗场景 | --- #### 三、策略二:堆内存 vs 堆外内存 | 内存类型 | 分配方法 | 实现原理 | 特点 | |----------------|------------------|--------------------------------------------------------------------------|--------------------------| | **堆内存** | `heapBuffer()` | 1. 池化:从 `heapArena` 分配<br>2. 非池化:创建 `UnpooledHeapByteBuf`[^3] | 受 JVM GC 管理,无需手动释放 | | **堆外内存** | `directBuffer()` | 1. 池化:从 `directArena` 分配<br>2. 非池化:创建 `UnpooledDirectByteBuf` | 需手动释放,减少 GC 压力[^3] | --- #### 四、核心分配流程(以池化分配为例) 1. **内存池初始化**: ```java // 创建包含多个Arena的内存池 heapArenas = new PoolArena[<numCores>]; directArenas = new PoolArena[<numCores>]; ``` 2. **线程局部缓存**: ```java // 每个线程绑定专属缓存 PoolThreadCache cache = threadCache.get(); if (cache == null) { cache = new PoolThreadCache(heapArena, directArena); } ``` 3. **内存分配步骤**: ```mermaid graph TD A[分配请求] --> B{大小 ≤ 16KB?} B -->|是| C[从 Tiny/Small 缓存分配] B -->|否| D[从 Normal 缓存分配] C --> E{分配成功?} D --> E E -->|是| F[返回内存块] E -->|否| G[从 Arena 主内存分配] G --> F ``` --- #### 五、策略选择机制 1. **智能分配策略**: ```java public ByteBuf buffer() { // 自动选择最优内存类型 if (PlatformDependent.hasUnsafe()) { return directBuffer(DEFAULT_INITIAL_CAPACITY); } return heapBuffer(DEFAULT_INITIAL_CAPACITY); } ``` 2. **I/O 优化策略**: ```java public ByteBuf ioBuffer() { // 优先分配堆外内存 if (PlatformDependent.hasUnsafe()) { return directBuffer(0); } return heapBuffer(0); } ``` --- #### 六、配置方式 通过 `ChannelOption` 动态配置: ```java // 使用池化堆外内存 bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); // 使用非池化堆内存 bootstrap.option(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false)); ``` --- ### 总结 Netty 的内存分配策略通过以下方式实现: 1. **分层抽象**:`ByteBufAllocator` → `AbstractByteBufAllocator` → 具体实现类 2. **策略模式**:池化/非池化、堆内/堆外作为可插拔策略 3. **智能路由**:根据平台特性调用方法自动选择最优策略 4. **线程优化**:通过 `PoolThreadCache` 减少线程竞争[^2] 这种设计使 Netty 能在不同场景下灵活切换内存策略,平衡性能资源消耗。[^1][^2][^3] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值