如何在linux下检测内存泄漏
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在linux下检测内存泄漏相关的知识,希望对你有一定的参考价值。
参考技术A 必须对程序中的内存分配和释放情况进行记录,所能够采取的办法就是重载所有形式的operator new 和 operator delete,截获 new operator 和 delete operator 执行过程中的内存操作信息。下面列出的就是重载形式void* operator new( size_t nSize, char* pszFileName, int nLineNum )void* operator new[]( size_t nSize, char* pszFileName, int nLineNum )void operator delete( void *ptr )void operator delete[]( void *ptr )我们为 operator new 定义了一个新的版本,除了必须的 size_t nSize 参数外,还增加了文件名和行号,这里的文件名和行号就是这次 new operator 操作符被调用时所在的文件名和行号,这个信息将在发现内存泄漏时输出,以帮助用户定位泄漏具体位置。对于 operator delete,因为无法为之定义新的版本,我们直接覆盖了全局的 operator delete 的两个版本。在重载的 operator new 函数版本中,我们将调用全局的 operator new 的相应的版本并将相应的 size_t 参数传入,而后,我们将全局 operator new 返回的指针值以及该次分配所在的文件名和行号信息记录下来,这里所采用的数据结构是一个 STL 的 map,以指针值为 key 值。当 operator delete 被调用时,如果调用方式正确的话(调用方式不正确的情况将在后面详细描述),我们就能以传入的指针值在 map 中找到相应的数据项并将之删除,而后调用 free 将指针所指向的内存块释放。当程序退出的时候,map 中的剩余的数据项就是我们企图检测的内存泄漏信息--已经在堆上分配但是尚未释放的分配信息。Netty源码-内存泄漏检测toLeakAwareBuffer
参考技术ANetty在实现 ByteBuf 时采用了引用计数法进行 ByteBuf 的回收,使用引用计数法进行回收的 ByteBuf 都扩展了 AbstractReferenceCountedByteBuf 类,在使用 AbstractReferenceCountedByteBuf 时需要调用 AbstractReferenceCountedByteBuf.retain 方法递增引用计数器,在使用完毕时则需要调用 AbstractReferenceCountedByteBuf.release 方法递减引用计数器,当计数器为 0 时,会进行 ByteBuf 的回收工作:池化的 ByteBuf 不会进行实际的内存释放,会将占用的内存归还给内存池,非池化的 ByteBuf 则会直接释放内存(为了叙述简单,后面释放内存则指真正释放内存或者将内存归还给内存池)。
通过上面的描述可知, ByteBuf 的正确回收依赖 retain 和 release 方法的正确调用,内存提前释放(即在使用 ByteBuf 时没有调用 retain 方法,导致提前释放)应用会报错,用户也能及时感知到;但是如果使用完 ByteBuf 忘了调用 release 则会导致内存不能及时得到回收,造成内存泄漏,且内存泄漏用户无法及时感知,久而久之就会发生OOM。为了解决这种问题,Netty采用了内存泄漏检测机制,发生内存泄漏时会通过日志将内存泄漏信息打印出来,报告给用户。
Netty的内存泄漏检测使用了 WeakReference ,即弱引用,了解过Java四种引用类型(强、软、弱、虚)和引用队列( ReferenceQueue )的读者知道,弱引用持有的对象会在虚拟机触发GC时(不管回收之后内存是否够用)被回收掉,如果使用具有引用队列参数的构造函数实例化 WeakReference 时,弱引用持有的对象在GC被回收时,弱引用自身会被放入引用队列。
为了后面能更好的理解Netty内存泄漏检测的细节,下面先看几个弱引用的例子,在下面的几个例子中,我们使用的数据类和自定义的弱引用类子类如下:
好了,三个例子已经介绍完毕,后面在介绍Netty内存泄漏检测时就使用了这里的例子结果,在具体介绍时会和这里的例子一一对应。
Netty中将普通 ByteBuf 转为具有内存泄漏检测功能的 ByteBuf 是通过 AbstractByteBufAllocator.toLeakAwareBuffer 方法实现的,我们直接在Eclipse中看该方法的调用层次即可知道Netty在哪里对 ByteBuf 进行了转换,该方法调用如下图所示:
可见池化内存分配器在分配heap或者direct ByteBuf 时都进行了转换,非池化内存分配器仅在分配direct ByteBuf 时进行了转换。个人理解时采用池化内存需要特别关注内存释放,否则为了实现池化内存预先分配的一大块内存会因为没有释放被很快分配完,造成后面没有内存进行分配。非池化分配的直接内存也需要特别注意释放,放置内存泄漏;非池化分配的heap内存(其实就是一个 byte 数组)则可以在对象被回收时同时被回收掉,发生内存泄漏的可能性较小。
本节介绍Netty中内存泄漏检测相关的类,仅做一个大致介绍,类中的重要方法我们放在后面介绍。
主要负责使用 track 方法对指定的 ByteBuf 进行内存检测泄漏进行追踪,并返回负责追踪的 ResourceLeakTracker 类实例,同时在调用 track 方法时,也会根据指定的检测级别汇报最近的内存泄漏检测结果。该类由工厂类 ResourceLeakDetectorFactory 负责实例化,默认的实现为 ResourceLeakDetector ,在 ResourceLeakDetectorFactory 类的默认实现 DefaultResourceLeakDetectorFactory 中,也会根据用户是否配置了 io.netty.customResourceLeakDetector 来决定采用默认实现 ResourceLeakDetector 还是使用用户自定义的 ResourceLeakDetector ,用户自定义的 ResourceLeakDetector 必须是其子类。
默认实现为 DefaultResourceLeak , DefaultResourceLeak 实现了 ResourceLeakTracker 和 ResourceLeak 接口,同时也继承了类 WeakReference ,是一个弱引用实现。首先,同上面 例2 的结果一样,如果在使用 ByteBuf 时忘了调用 AbstractReferenceCountedByteBuf.release 方法,那么将不会调用 DefaultResourceLeak.clear 方法去手动清空该弱引用持有的实际对象,在发生GC时,会由垃圾收集器对弱引用持有的实际对象进行回收,即发生了内存泄漏,同时该弱引用自身也会被加入到引用队列中,该引用队列是 ResourceLeakDetector 的成员域,上面介绍 ResourceLeakDetector 类时说到该类会在用户 track 指定 ByteBuf 是汇报检测结果,该类的汇报数据来源就是引用队列。 DefaultResourceLeak 同时还提供了 record 方法可以让用户在指定时机选择调用,这个方法可以记录用户的调用轨迹(堆栈)。 Record 同时也是一种单链表,在 DefaultResourceLeak 中就使用单链表记录用户的调用轨迹。
DefaultResourceLeak 供用户记录程序调用轨迹的类,也就是 DefaultResourceLeak.record 方法返回的对象,继承自 Throwable ,因此可以使用 Throwable.getStackTrace 方法获得调用轨迹信息,打印在内存泄漏报告中可以让用户更好的排除内存泄漏问题。
在上面介绍 ResourceLeakTracker 时,说到其默认实现为 DefaultResourceLeak , DefaultResourceLeak 提供了 record 方法记录用户的调用轨迹,用户可在调用 ByteBuf 方法时调用 record 方法记录调用轨迹,调用的频率越多,后面在汇报内存泄漏情况时就能打印出越详细的信息,这样也能更方便的排查问题。
Netty提供了两个 ByteBuf 的封装类供选择,就对应不同的 record 调用频率,每个封装类都持有 ResourceLeakTracker 对象,Netty根据配置的内存检测级别(下一节介绍相关配置参数)使用不同的 ByteBuf 封装类。
Netty提供的两个 ByteBuf 封装类就是 SimpleLeakAwareCompositeByteBuf 和 AdvancedLeakAwareCompositeByteBuf , AdvancedLeakAwareCompositeByteBuf 是 SimpleLeakAwareCompositeByteBuf 的子类, SimpleLeakAwareCompositeByteBuf 类仅仅持有 ResourceLeakTracker 对象,但是看其源码,发现没有调用过 record 方法,所以只能知道是否发生了内存泄漏时,无法打印出任何调用轨迹信息。 AdvancedLeakAwareCompositeByteBuf 作为 SimpleLeakAwareCompositeByteBuf 的子类,在 ByteBuf 的多个方法中调用了 record 方法,所以在发生内存泄漏时,能够打印出比较详细的调用轨迹信息。
在 AdvancedLeakAwareCompositeByteBuf 类中使用了配置参数 io.netty.leakDetection.acquireAndReleaseOnly 来控制是否只是在调用增加或减少引用计数器的方法时才调用 record 方法记录调用轨迹,默认为false。 AdvancedLeakAwareCompositeByteBuf 中 retain 和 release 方法因为改变了引用计数器就直接调用了 record 方法,而该类中的其他方法则根据 io.netty.leakDetection.acquireAndReleaseOnly 的配置决定是否调用 record 方法,这里为了节省篇幅就不列出 AdvancedLeakAwareCompositeByteBuf 类中调用 record 的方法了,读者可自行查看。
在介绍相关配置参数之前,我们先看下Netty提供的内存泄漏检测级别:
Level.ADVANCED 和 Level.PARANOID 使用的 ByteBuf 包装类都是 AdvancedLeakAwareCompositeByteBuf ,我们上面介绍 ResourceLeakDetector 类时提到该类使用 track 方法对指定的 ByteBuf 进行内存检测泄漏进行追踪,并返回负责追踪的 ResourceLeakTracker 类实例,同时在调用 track 方法时,也会根据指定的检测级别汇报最近的内存泄漏检测结果。如果内存泄漏检测级别为 Level.PARANOID 时则每次调用 track 方法都会进行内存泄漏报告;如果级别为 Level.ADVANCED 或者 Level.SIMPLE 则会以一定频率进行内存泄漏报告,而不是每次 track 都进行报告。
是否关闭Netty内存泄漏检测功能,默认为false。如果该参数配置为false,则默认的内存泄漏检测级别根据此参数的配置为 Level.DISABLED ,否则默认的级别为 Level.SIMPLE 。
配置内存泄漏检测级别的参数,用于老版本的配置参数。
新的内存泄漏检测级别参数,如果没有配置,则会采用老版本参数配置的级别作为最终配置。
在第4节介绍内存泄漏检测相关类时,我们介绍过 DefaultResourceLeak 提供了 record 方法记录用户的调用轨迹,如果当前保存的调用轨迹记录数 Record 大于参数 io.netty.leakDetection.targetRecords 配置的值,那么会以一定的概率(1/2^n)删除头结点之后再加入新的记录,当然也有可能不删除头结点直接新增新的记录。
该参数的默认为4。
上面介绍过,在 AdvancedLeakAwareCompositeByteBuf 类中使用了配置参数 io.netty.leakDetection.acquireAndReleaseOnly 来控制是否只是在调用增加或减少引用计数器的方法时才调用 record 方法记录调用轨迹,默认为false。
在介绍 ResourceLeakDetector 类时提到过,默认的 ResourceLeakDetector 类就是 ResourceLeakDetector ,但是用户可以使用参数 io.netty.customResourceLeakDetector 来决定采用默认实现 ResourceLeakDetector 还是使用用户自定义的 ResourceLeakDetector 。
我们在第二节介绍了Netty中将普通 ByteBuf 转为具有内存泄漏检测功能的 ByteBuf 是通过 AbstractByteBufAllocator.toLeakAwareBuffer 方法实现的。
这里我们先看下该方法的源码:
上面的源码中是调用 AbstractByteBuf.leakDetector.track(buf) 返回 ResourceLeakTracker 类对象的,这里我们看下默认的 ResourceLeakDetector 中 track 方法实现:
我们看到 AbstractByteBufAllocator.toLeakAwareBuffer 对 ResourceLeakDetector.track 返回的 DefaultResourceLeak 和传入的 ByteBuf 对象进行封装,返回了具有内存泄漏检测功能的 ByteBuf 封装类 SimpleLeakAwareCompositeByteBuf 或其子类 AdvancedLeakAwareCompositeByteBuf 。如果应用程序在使用 ByteBuf 正确调用了 retain 和 release 方法,则在引用计数器为0时,则会清除弱引用持有的实际对象,发生GC时, DefaultResourceLeak 也不会被放入引用队列中(见前面第2节 例3 结果)。
但是如果应用程序在使用 ByteBuf 没有正确调用 retain 和 release 方法,则不会清除弱引用持有的实际对象,此时如果实际上已经没有强引用指向该 ByteBuf ,那么在发生GC时,垃圾收集器会回收该 ByteBuf ,而弱引用 DefaultResourceLeak 会被放入引用队列中(见前面第2节 例2 结果),加入到引用队列中的就是识别到的发生内存泄漏的 ByteBuf 。在 ResourceLeakDetector.track 方法中调用的 reportLeak 输出的就是引用队列中的弱引用 DefaultResourceLeak :
到这里,已经基本上介绍完Netty内存检测的实现原理,下面我们再看下 DefaultResourceLeak.record 是如何记录调用轨迹的:
最后我们再看下 Record 是如何输出调用轨迹的,前面我们说到 Record 继承自类 Throwable ,因此可使用 getStackTrace 方法获取实例化该对象时的调用轨迹,所以上面在输出内存泄漏报告时就调用了 Record.toString 方法:
以上是关于如何在linux下检测内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章