netty5学习笔记-内存泄露检测

netty中用到内存泄露检测的地方主要有:1、CompositeByteBuf;2、HashedWheelTimer;3、继承AbstractByteBufAllocator的几个类。

下面我们看看netty里的内存检测类ResourceLeakDetector的具体实现:

 

netty的内存泄露检测分为四级:

DISABLED: 不进行内存泄露的检测;

SIMPLE: 抽样检测,且只对部分方法调用进行记录,消耗较小,有泄漏时可能会延迟报告,默认级别;

ADVANCED: 抽样检测,记录对象最近几次的调用记录,有泄漏时可能会延迟报告;

PARANOID:每次创建一个对象时都进行泄露检测,且会记录对象最近的详细调用记录。是比较激进的内存泄露检测级别,消耗最大,建议只在测试时使用。

如果需要修改默认的检测级别,可以通过:1、调用静态方法setLevel进行修改;2、设置启动参数io.netty.leakDetectionLevel。

由于内存泄露主要是对某一类资源的检测,因此对于同一类的对象,只需实例化一个ResourceLeakDetector, 否则起不到检测的作用。

 

public class HashedWheelTimer implements Timer {
    ...
    private static final ResourceLeakDetector leakDetector =
            new ResourceLeakDetector(
                    HashedWheelTimer.class, 1, Runtime.getRuntime().availableProcessors() * 4);
    ...

初始化的时候需要设置被检测的类(或其他文字标记)、抽样间隔、最大活跃对象数

 

 

public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
        if (resourceType == null) {
            throw new NullPointerException("resourceType");
        }
        if (samplingInterval <= 0) {
            throw new IllegalArgumentException("samplingInterval: " + samplingInterval + " (expected: 1+)");
        }
        if (maxActive <= 0) {
            throw new IllegalArgumentException("maxActive: " + maxActive + " (expected: 1+)");
        }

        this.resourceType = resourceType;
        // 抽样间隔,当基本为SIMPLE或ADVANCED时,每创建samplingInterval个对象进行一次记录。
        this.samplingInterval = samplingInterval;
        // 最大活跃对象数,超过这个值就会进行对应处理(如报警或主动关闭资源)
        this.maxActive = maxActive;

        head.next = tail;
        tail.prev = head;
    }

每次对象创建的时候都需要调用open方法:

 

 

public ResourceLeak open(T obj) {
        Level level = ResourceLeakDetector.level;
	// 关闭检测
        if (level == Level.DISABLED) {
            return null;
        }

        if (level.ordinal() < Level.PARANOID.ordinal()) {
           // 小于PARANOID及ADVANCED和SIMPLE, 每创建samplingInterval个对象调用一次reportLeak
	    if (leakCheckCnt ++ % samplingInterval == 0) {
                reportLeak(level);
                return new DefaultResourceLeak(obj);
            } else {
                return null;
            }
        } else {
	    // PARANOID级别每次都调用reportLeak
            reportLeak(level);
            return new DefaultResourceLeak(obj);
        }
    }
	private void reportLeak(Level level) {
	// 内存泄露的主要报告方式为日志,因此如果日志级别不够,则只进行数据处理,不走具体的报告分支
    if (!logger.isErrorEnabled()) {
      for (;;) {
	// 这个refQueue里面主要是被垃圾回收的对象,垃圾回收过的对象如果保存到refQueue不是本次讨论的重点,有兴趣可以搜索PhantomReference,ReferenceQueue相关文章
        @SuppressWarnings("unchecked")
        DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
        if (ref == null) {
          break;
        }
	// 对回收的对象调用close方法,这个方法会减少active的记数
        ref.close();
      }
      return;
    }


    // 非PARANOID级别每隔sampleInterval记录一次,因此这里又乘以sampleInterval,使抽样的情况下偏差尽可能小
	    int samplingInterval = level == Level.PARANOID? 1 : this.samplingInterval;
    if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(false, true)) {
	// 活跃数大于maxActive则进行报警,且只报一次
      logger.error("LEAK: You are creating too many " + resourceType + " instances. " +
          resourceType + " is a shared resource that must be reused across the JVM," +
          "so that only a few instances are created.");
    }


    // Detect and report previous leaks.
    for (;;) {
      @SuppressWarnings("unchecked")
      DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
      if (ref == null) {
        break;
      }


      ref.clear();

// 调用close方法,确保该资源被移除,如果返回true,表明资源虽然被垃圾回收掉了,但是没有在应用中显式的调用close方法,后面会在日志中警告用户通过更高的基本来进行更详细的分析
      if (!ref.close()) {
        continue;
      }


      String records = ref.toString();
      if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
        if (records.isEmpty()) {
          logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
              "Enable advanced leak reporting to find out where the leak occurred. " +
              "To enable advanced leak reporting, " +
              "specify the JVM option '-D{}={}' or call {}.setLevel() " +
              "See http://netty.io/wiki/reference-counted-objects.html for more information.",
              resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
        } else {
          logger.error(
              "LEAK: {}.release() was not called before it's garbage-collected. " +
              "See http://netty.io/wiki/reference-counted-objects.html for more information.{}",
              resourceType, records);
        }
      }
    }
  }

open方法调用后会返回一个ResourceLeak对象,应用可以通过该对象的record方法记录调用详情,同时通过close方法通知资源的释放。

private void record0(Object hint, int recordsToSkip) {
            if (creationRecord != null) {
	        // 得到当前的调用栈信息,由于这里的执行比较耗时,所以频繁调用对应用是有明显的性能损耗的,因此netty中的默认级别是SIMPLE
                String value = newRecord(hint, recordsToSkip);
                // 将信息记录到lastRecords中,并保持最多MAX_RECORDS条记录,该信息会在检测到内存泄露时打印到日志中
                synchronized (lastRecords) {
                    int size = lastRecords.size();
                    if (size == 0 || !lastRecords.getLast().equals(value)) {
                        lastRecords.add(value);
                    }
                    if (size > MAX_RECORDS) {
                        lastRecords.removeFirst();
                    }
                }
            }
        }
	public boolean close() {
	      // 保证一个对象只执行一次close方法
      if (freed.compareAndSet(false, true)) {
        synchronized (head) {
          active --;
          prev.next = next;
          next.prev = prev;
          prev = null;
          next = null;
        }
        return true;
      }
      return false;
    }

内存相关泄露的检测实现本身比较简单,即打开资源时增加一个活跃对象数,释放一次资源时减少一个活跃对象数,如果活跃对象数超过阈值则报告异常。但需要注意的是如果要做到有意义的内存检测则需要遵循新建对象调用ResourceLeakDetector.open方法,释放对象调用ResourceLeak.close,否则可能会出现误报的情况。同时激进的内存检测对性能有很大影响,在生产环境下尽量不要打开。

好了,ResourceLeakDetector的分析到这里基本上结束了,但是还存在一个问题,active、head、tail的访问使用了synchronized保证线程安全,然而 leakCheckCnt的操作却是线程不安全的,如果有多个线程同时操作leakCheckCnt的结果是不准确的。默认情况下由于操作的LEVEL是SIMPLE,默认的interval也比较大(113), leakCheckCnt是一个递增的操作,即使出错,也并不会造成严重的影响,只是会影响报错的及时性而已,估计是基于这几个考虑,并没对该字段同步或者使用AtomicLong之类的类代替。

 

你可能感兴趣的:(netty)