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

2019独角兽企业重金招聘Python工程师标准>>> netty中的bytebuf(脱离JVM的垃圾收集实现手动回收)及引用和内存泄漏
文章图片

从内存分配的角度看,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。
【netty中的bytebuf(脱离JVM的垃圾收集实现手动回收)及引用和内存泄漏】
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 DatagramPacket, HttpContent, 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

    推荐阅读