Netty如何监控内存泄露

前言

一般而言,在Netty程序中都会采用池化的ByteBuf,也就是PooledByteBuf以提高程序性能。但是PooledByteBuf需要在使用完毕后手工释放,否则就会因为PooledByteBuf申请的内存空间没有归还进而造成内存泄露,最终OOM。而一旦泄露发生,在复杂的应用程序中找到未手工释放的ByteBuf并不是一个简单的活计,在没有工具辅助的情况只能白盒检查所有源码,效率无疑十分低下。

为了解决这个问题,Netty设计了专门的泄露检测接口用于实现对需要手动释放的资源对象的监控。

JDK的弱引用和引用队列

在分析Netty的泄露监控功能之前,先来复习下其中会用到的JDK知识:引用。

在java中存在4中引用类型,分别是强引用,软引用,弱引用,虚引用。

强引用

强引用,是我们写程序最经常使用的方式。比如一个将一个值赋给一个变量,那这个对象值就被该变量强引用了。除非设置为null,否则java的内存回收不会回收该对象。就算是内存不足异常发生也不会。

软引用

软引用所引用的对象会在java内存不足的时候,被gc回收。如果gc发生的时候,java的内存还充足则不会回收这个对象

使用的方式如下

  • SoftReference ref = new SoftReference(new Date());
  • Date tmp = ref.get(); //如果对象没有被回收,则这个get操作会返回初始化的值。如果被回收了之后,则返回null

弱引用

弱引用则比软引用更差一些。只要是gc发生的时候,弱引用的对象都会被回收。使用方式上和软引用类似,如下

  • WeakReference re = new WeakReference(new Date());
  • re.get();

虚引用

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

除了强引用之外,其余的引用都有一个引用队列可以与之配合。当java清理调用不必要的引用后,会将这个引用本身(不是引用指向的值对象)添加到队列之中。代码如下

ReferenceQueue<Date> queue = new ReferenceQueue<>();
WeakReference<Date> re = new WeakReference<Date>(new Date(), queue);
Reference<? extends Date> moved = queue.poll();

从上面的介绍可以看出引用队列的一个适用场景:与弱引用或虚引用配合,监控一个对象是否被GC回收

Netty的实现思路

针对需要手动关闭的资源对象,Netty设计了一个接口io.netty.util.ResourceLeakTracker来实现对资源对象的追踪。该接口提供了一个release方法。在资源对象关闭需要调用release方法。如果从未调用release方法则被认为存在资源泄露。

该接口只有一个实现,就是io.netty.util.ResourceLeakDetector.DefaultResourceLeak,该实现继承了WeakReference。每一个DefaultResourceLeak会与一个需要监控的资源对象关联,同时关联着一个引用队列。

当资源对象被GC回收后,与之关联的DefaultResourceLeak就会进入引用队列。通过检查引用队列中的DefaultResourceLeak实例的状态(release方法的调用会导致状态变更),就能确定在资源对象被GC前,是否执行了手动关闭的相关方法,从而判断是否存在泄漏可能。

代码实现

分配监控对象

当进行ByteBuf的分配的时候,比如方法io.netty.buffer.PooledByteBufAllocator#newHeapBuffer,查看代码如下

protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
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);
}

当实际持有内存区域的ByteBuf生成,通过方法io.netty.buffer.AbstractByteBufAllocator#toLeakAwareBuffer(io.netty.buffer.ByteBuf)加持监控泄露的能力。该方法代码如下

protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
ResourceLeakTracker<ByteBuf> leak;
switch (ResourceLeakDetector.getLevel()) {
case SIMPLE:
leak = AbstractByteBuf.leakDetector.track(buf);
if (leak != null) {
buf = new SimpleLeakAwareByteBuf(buf, leak);
}
break;
case ADVANCED:
case PARANOID:
leak = AbstractByteBuf.leakDetector.track(buf);
if (leak != null) {
buf = new AdvancedLeakAwareByteBuf(buf, leak);
}
break;
default:
break;
}
return buf;
}

根据不同的监控级别生成不同的监控等级对象。Netty对监控分为4个等级:

  1. 关闭:这种模式下不进行泄露监控。
  2. 简单:这种模式下以1/128的概率抽取ByteBuf进行泄露监控。
  3. 增强:在简单的基础上,每一次对ByteBuf的调用都会尝试记录调用轨迹,消耗较大。
  4. 偏执:在增强的基础上,对每一个ByteBuf都进行泄露监控,消耗最大。

一般而言,在项目的初期使用简单模式进行监控,如果没有问题一段时间后就可以关闭。否则升级到增强或者偏执模式尝试确认泄露位置。

追踪和检查泄露

泄露的检查和追踪主要依靠两个类io.netty.util.ResourceLeakDetector.DefaultResourceLeakio.netty.util.ResourceLeakDetector.前者用于追踪一个资源对象,并且记录对应的调用轨迹;后者则负责管理和生成DefaultResourceLeak对象。

DefaultResourceLeak

首先来看用于追踪资源对象的监控对象。该类继承了WeakReference,有几个重要的属性,如下

//存储着最新的调用轨迹信息,record内部通过next指针形成一个单向链表
private volatile Record head;
//调用轨迹不会无限制的存储,有一个上限阀值。超过了阀值会抛弃掉一些调用轨迹信息。
private volatile int droppedRecords;
//存储着所有的追踪对象,用于确认追踪对象是否处于可用。
private final Set<DefaultResourceLeak<?>> allLeaks;
//记录追踪对象的hash值,用于后续操作中的对象对比。
private final int trackedHash;

这个类的作用有三个:

  1. 调用record方法记录调用轨迹
  2. 调用close方法结束追踪
  3. 以及本身作为WeakReference,在追踪对象被GC回收后自身被入列到ReferenceQueue中。

先来看下record方法,代码如下

@Override
public void record() {
record0(null);
}
@Override
public void record(Object hint) {
record0(hint);
}
private void record0(Object hint) {
if (TARGET_RECORDS > 0) {
Record oldHead;
Record prevHead;
Record newHead;
boolean dropped;
do {
if ((prevHead = oldHead = headUpdater.get(this)) == null) {
// already closed.
return;
}
final int numElements = oldHead.pos + 1;
if (numElements >= TARGET_RECORDS) {
final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
prevHead = oldHead.next;
}
} else {
dropped = false;
}
newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead);
} while (!headUpdater.compareAndSet(this, oldHead, newHead));
if (dropped) {
droppedRecordsUpdater.incrementAndGet(this);
}
}
}

方法record0的思路总结下也很简单,概括如下:

  1. 使用CAS方式当前的调用轨迹对象Record设置为head属性的值。
  2. Record对象中的pos属性记录着当前轨迹链的长度,当追踪对象的轨迹队链的长度超过配置值时,有一定的几率(1-1/2min(n-target_record,30))将最新的轨迹对象从链条中删除。
  3. CAS成功后,如果有抛弃头部的轨迹对象,则抛弃计数+1。

步骤2中在链条过长时选择删除最新的轨迹对象是基于以下两点出发:

  1. 一般泄漏都发生在最后一次使用后忘记调用释放方法造成,因此替换最新的归集对象,并不会造成判断信息的丢失
  2. 一般而言,关注泄漏对象,也需要了解对象实例的申请位置,因此删除节点时不能从头开始删除。

在来看看close方法。代码如下

public boolean close(T trackedObject) {
assert trackedHash == System.identityHashCode(trackedObject);
try {
return close();
} finally {
reachabilityFence0(trackedObject);
}
}
public boolean close() {
if (allLeaks.remove(this)) {
// Call clear so the reference is not even enqueued.
clear();
headUpdater.set(this, null);
return true;
}
return false;
}
private static void reachabilityFence0(Object ref) {
if (ref != null) {
synchronized (ref) {
}
}
}

close方法本身没有什么,就是将资源进行了清除。需要解释的是方法reachabilityFence0。不过该方法需要在下文的报告泄露中才会具备作用,这边先暂留。

ResourceLeakDetector

该类用于按照规则进行追踪对象的生成,外部主要是调用其方法track,代码如下

public final ResourceLeakTracker<T> track(T obj) {
return track0(obj);
}
private DefaultResourceLeak track0(T obj) {
Level level = ResourceLeakDetector.level;
if (level == Level.DISABLED) {
return null;
}
if (level.ordinal() < Level.PARANOID.ordinal()) {
if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
reportLeak();
return new DefaultResourceLeak(obj, refQueue, allLeaks);
}
return null;
}
reportLeak();
return new DefaultResourceLeak(obj, refQueue, allLeaks);
}

从生成策略来看,只要是小于PARANOID级别都是抽样生成。生成的追踪对象上一个章节已经分析过了,这边主要来看reportLeak方法,如下

private void reportLeak() {
if (!logger.isErrorEnabled()) {
clearRefQueue();
return;
}
// Detect and report previous leaks.
for (;;) {
@SuppressWarnings("unchecked")
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
//返回true意味着资源没有调用close或者dispose方法结束追踪就被GC了,意味着该资源存在泄漏。
if (!ref.dispose()) {
continue;
}
String records = ref.toString();
if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
if (records.isEmpty()) {
reportUntracedLeak(resourceType);
} else {
reportTracedLeak(resourceType, records);
}
}
}
}
boolean io.netty.util.ResourceLeakDetector.DefaultResourceLeak#dispose() {
clear();
return allLeaks.remove(this);
}

可以看到,每次生成资源追踪对象时,都会遍历引用队列,如果发现泄漏对象,则进行日志输出。

这里面有个细节的设计点在于DefaultResourceLeak进入引用队列并不意味着一定内存泄露。判断追踪对象是否泄漏的规则是对象在被GC之前是否调用了DefaultResourceLeakclose方法。举个例子,PooledByteBuf只要将自身持有的内存释放回池化区就算是正确的释放,其后其实例对象可以被GC回收掉。

因此方法reportLeak在遍历引用队列时,需要通过调用dispose方法来确认追踪对象的dispose是否调用或者close方法是否被调用过。如果dispose方法返回true,则意味着被追踪对象未调用关闭方法就被GC,那就意味着造成了泄露。

上个章节曾提到的一个方法reachabilityFence0

在JVM的规定中,如果一个实例对象不再被需要,则可以判定为可回收。即使该实例对象的一个具体方法正在执行过程中,也是可以的。更确切一些的说,如果一个实例对象的方法体中,不再需要读取或者写入实例对象的属性,则此时JVM可以回收该对象,即使方法还没有完成。

然而这样会导致一个问题,在close方法中,如果close方法还没有执行完毕,trackedObject对象实例就被GC回收了,就会导致DefaultResourceLeak对象被加入到引用队列中,从而可能在reportLeak方法调用中触发方法dispose,假设此时close方法才刚开始执行,则dispose方法可能返回true。程序就会判定这个对象出现了泄露,然而实际上却没有。

要解决这个问题,只需要让close方法执行完毕前,让对象不要回收即可。reachabilityFence0方法就完成了这个作用。


文章原创首发于公众号:林斌说Java,转载请注明来源,谢谢。

欢迎扫码关注

Netty如何监控内存泄露的更多相关文章

  1. Netty堆外内存泄露排查与总结

    导读 Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程. Netty 底层基于 JDK ...

  2. Netty堆外内存泄漏排查,这一篇全讲清楚了

    上篇文章介绍了Netty内存模型原理,由于Netty在使用不当会导致堆外内存泄漏,网上关于这方面的资料比较少,所以写下这篇文章,专门介绍排查Netty堆外内存相关的知识点,诊断工具,以及排查思路提供参 ...

  3. <JVM下篇:性能监控与调优篇>补充:浅堆深堆与内存泄露

    笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机) 同步更新:https://gitee.com/vectorx/NOTE_JVM https://codechina.cs ...

  4. 从一次netty 内存泄露问题来看netty对POST请求的解析

    背景 最近生产环境一个基于 netty 的网关服务频繁 full gc 观察内存占用,并把时间维度拉的比较长,可以看到可用内存有明显的下降趋势 出现这种情况,按往常的经验,多半是内存泄露了 问题定位 ...

  5. 抓到 Netty 一个隐藏很深的内存泄露 Bug | 详解 Recycler 对象池的精妙设计与实现

    欢迎关注公众号:bin的技术小屋,如果大家在看文章的时候发现图片加载不了,可以到公众号查看原文 本系列Netty源码解析文章基于 4.1.56.Final版本 最近在 Review Netty 代码的 ...

  6. java: web应用中不经意的内存泄露

    前面有一篇讲解如何在spring mvc web应用中一启动就执行某些逻辑,今天无意发现如果使用不当,很容易引起内存泄露,测试代码如下: 1.定义一个类App package com.cnblogs. ...

  7. C++11 shared_ptr 智能指针 的使用,避免内存泄露

    多线程程序经常会遇到在某个线程A创建了一个对象,这个对象需要在线程B使用, 在没有shared_ptr时,因为线程A,B结束时间不确定,即在A或B线程先释放这个对象都有可能造成另一个线程崩溃, 所以为 ...

  8. LeakCanary Android 和 Java 内存泄露检测。

    开始使用 在 build.gradle 中加入引用,不同的编译使用不同的引用: dependencies { debugCompile 'com.squareup.leakcanary:leakcan ...

  9. Android开发笔记——常见BUG类型之内存泄露与线程安全

    本文内容来源于最近一次内部分享的总结,没来得及详细整理,见谅. 本次分享主要对内存泄露和线程安全这两个问题进行一些说明,内部代码扫描发现的BUG大致分为四类:1)空指针:2)除0:3)内存.资源泄露: ...

随机推荐

  1. SpringBoot+Vue+WebSocket 实现在线聊天

    一.前言 本文将基于 SpringBoot + Vue + WebSocket 实现一个简单的在线聊天功能 页面如下: 在线体验地址:http://www.zhengqingya.com:8101 二 ...

  2. 最小化安装CentOS 7 系统

    目录 CentOS 程序准备 开始安装系统 创建虚拟机 安装系统 CentOS 运维最常接触的系统就是CentOS系统,无论是版本 6 还是版本 7 而且在安装系统时,讲究最小化安装系统,之后当需要什 ...

  3. df命令、du命令、磁盘分区 使用介绍

    第4周第1次课(4月9日) 课程内容:4.1 df命令4.2 du命令4.3/4.4 磁盘分区 4.1 df命令 汇报磁盘空间使用情况,linux磁盘挂载点是无法直接访问的. df 和 df -h区别 ...

  4. CSS与JavaScript小结

    一.css 全称Cascading Style Sheets,层叠样式表,具体的作用是美化页面,让页面中显得更加美观. 1.使用方式 在HTML页面中有三个地方可以使用,分别是标签内,头部标签中以及在 ...

  5. Python之HTTP静态Web服务器开发

    众所周知,Http协议是基于Tcp协议的基础上产生的浏览器到服务器的通信协议 ,其根本原理也是通过socket进行通信. 使用HTTP协议通信,需要注意其返回的响应报文格式不能有任何问题. 响应报文, ...

  6. 转:使用JSR-303进行校验 @Valid

    一.在SringMVC中使用 使用注解 1.准备校验时使用的JAR validation-api-1.0.0.GA.jar:JDK的接口: hibernate-validator-4.2.0.Fina ...

  7. 洛谷 题解 2165 [AHOI2009]飞行棋

    本蒟蒻又来发题解了, 看到这个题目,本蒟蒻直接开始推公式.. 嗯,可以通过弧长,推出弦长(l = 2 * r * cos(90 * l / (r * Π)); 然后对比各条弦长的平方和与直径的平方. ...

  8. luogu P3984 高兴的津津

    题目描述 津津上高中了.她在自己的妈妈的魔鬼训练下,成为了一个神犇,每次参加一次OI比赛必拿Au虐全场.每次她拿到一个Au后就很高兴.假设津津不会因为其它事高兴,并且她的高兴会持续T天(包包含获奖当天 ...

  9. 自学PHP的第22天---ThinkPHP中的路由、ThinkPHP目录结构

    这一切的一切都得从“Hello world”说起!!! 有很多东西在thinkPHP的官方开发文档上其实都有讲到,我在这里只是想记录自己每天坚持学习PHP的情况,今天接触ThinkPHP的路由,路由这 ...

  10. Django中直接执行SQL语句

    欢迎加入python学习交流群 667279387 今天在django views.py看到同事写的代码里面有段关于数据库查询的语句.因为涉及多个表的查询,所以django 的models的查询无法满 ...