在不再使用时调用finalize-resources自动关闭资源-避免对长期对象使用软引用和弱引用-识别和避免Java内存泄漏的最佳实践-使用try-注意lambda表达式的引用捕获-with (不再使用时间银行)
简介
在 Java 中,内存泄漏是指程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被废品回收器回收,最终导致内存占用不断增加,进而影响程序的性能和稳定性。
原因
内存泄漏通常由以下几个原因造成:-
对象生命周期管理不当
如果程序中存在一些长时间存在的对象,但在其不再使用时没有及时释放,就会导致内存泄漏。例如,没有及时关闭数据库连接、文件流或网络连接等资源。
-
静态集合引用
如果将对象存储在静态集合中,并且忘记从集合中删除不再需要的对象,那么这些对象将一直存在于内存中,无法被废品回收。
-
匿名内部类引用
如果在匿名内部类中引用了外部类的实例,而该匿名内部类的生命周期比外部类更长,就会导致外部类无法被废品回收。
-
废品回收机制失效
如果存在代码逻辑错误,导致废品回收机制无法正确标记和回收不再使用的对象,就会发生内存泄漏。
避免和解决
为了避免和解决内存泄漏问题,可以采取以下策略:-
及时释放资源
在使用完资源后,要确保及时关闭数据库连接、文件流、网络连接等资源。可以使用 try-with-resources 语句来自动关闭资源,或者在 finally 块中手动关闭资源。
-
使用弱引用(WeakReference)
如果无法避免长时间持有对象的引用,可以考虑使用弱引用。弱引用不会阻止对象被废品回收,当对象只被弱引用引用时,废品回收器会立即回收该对象。
-
尽早释放不再使用的对象
在程序中,要尽可能及时释放不再使用的对象。可以通过将对象置为 null 来断开对其的引用,从而帮助废品回收器确定该对象可以被回收。
-
避免滥用静态变量和集合
静态变量和集合容易导致对象泄漏。应该合理使用静态变量和集合,并在不再需要时及时清理其中的对象引用。
-
使用 Profiler 工具进行性能分析
使用专业的性能分析工具,如 VisualVM、JProfiler 等,可以帮助检测和识别内存泄漏问题。这些工具可以提供详细的堆内存使用情况和对象引用关系,帮助定位问题所在。
-
编写单元测试
编写全面的单元测试可以帮助发现潜在的内存泄漏问题。通过模拟不同的使用场景和输入条件,可以验证程序在不同情况下的内存使用情况,并及时修复潜在的泄漏问题。
-
定期进行代码审查
定期进行代码审查可以发现代码中存在的潜在内存泄漏问题。多人合作审查代码,可以从不同的角度发现问题,并制定相应的解决方案。
-
使用内存分析工具
使用内存分析工具(如 EclipseMemoryAnalyzer、MAT)可以帮助检测和分析内存泄漏问题。这些工具可以提供详细的内存快照,帮助分析对象的引用链和内存占用情况,从而找到内存泄漏的根本原因。
总结
避免和解决内存泄漏问题需要开发人员具备良好的资源管理意识和代码质量意识。及时释放资源、合理使用静态变量和集合、使用弱引用、编写测试和代码审查等都是有效的方法。同时,利用工具进行性能分析和内存分析可以帮助定位和解决内存泄漏问题。怎样解决Java中内存泄露
一旦知道确实发生了内存泄漏,就需要更专业的工具来查明为什么会发生泄漏。 JVM自己是不会告诉您的。 这些专业工具从JVM获得内存系统信息的方法基本上有两种:JVMTI和字节码技术(byte code instrumentation)。 Java虚拟机工具接口(Java Virtual Machine Tools Interface,JVMTI)及其前身Java虚拟机监视程序接口(Java Virtual Machine Profiling Interface,JVMPI)是外部工具与JVM通信并从JVM收集信息的标准化接口。 字节码技术是指使用探测器处理字节码以获得工具所需的信息的技术。 Optimizeit是Borland公司的产品,主要用于协助对软件系统进行代码优化和故障诊断,其中的Optimizeit Profiler主要用于内存泄漏的分析。 Profiler的堆视图就是用来观察系统运行使用的内存大小和各个类的实例分配的个数的。 首先,Profiler会进行趋势分析,找出是哪个类的对象在泄漏。 系统运行长时间后可以得到四个内存快照。 对这四个内存快照进行综合分析,如果每一次快照的内存使用都比上一次有增长,可以认定系统存在内存泄漏,找出在四个快照中实例个数都保持增长的类,这些类可以初步被认定为存在泄漏。 通过数据收集和初步分析,可以得出初步结论:系统是否存在内存泄漏和哪些对象存在泄漏(被泄漏)。 接下来,看看有哪些其他的类与泄漏的类的对象相关联。 前面已经谈到Java中的内存泄漏就是无用的对象保持,简单地说就是因为编码的错误导致了一条本来不应该存在的引用链的存在(从而导致了被引用的对象无法释放),因此内存泄漏分析的任务就是找出这条多余的引用链,并找到其形成的原因。 查看对象分配到哪里是很有用的。 同时只知道它们如何与其他对象相关联(即哪些对象引用了它们)是不够的,关于它们在何处创建的信息也很有用。 最后,进一步研究单个对象,看看它们是如何互相关联的。 借助于Profiler工具,应用程序中的代码可以在分配时进行动态添加,以创建堆栈跟踪。 也有可以对系统中所有对象分配进行动态的堆栈跟踪。 这些堆栈跟踪可以在工具中进行累积和分析。 对每个被泄漏的实例对象,必然存在一条从某个牵引对象出发到达该对象的引用链。 处于堆栈空间的牵引对象在被从栈中弹出后就失去其牵引的能力,变为非牵引对象。 因此,在长时间的运行后,被泄露的对象基本上都是被作为类的静态变量的牵引对象牵引。 总而言之, Java虽然有自动回收管理内存的功能,但内存泄漏也是不容忽视,它往往是破坏系统稳定性的重要因素。
Netty源码-内存泄漏检测toLeakAwareBuffer
Netty在实现 ByteBuf 时采用了引用计数法进行 ByteBuf 的回收,使用引用计数法进行回收的 ByteBuf 都扩展了 AbstractReferenceCountedByteBuf 类,在使用 AbstractReferenceCountedByteBuf 时需要调用 方法递增引用计数器,在使用完毕时则需要调用 方法递减引用计数器,当计数器为 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 是通过 方法实现的,我们直接在Eclipse中看该方法的调用层次即可知道Netty在哪里对 ByteBuf 进行了转换,该方法调用如下图所示:
可见池化内存分配器在分配heap或者directByteBuf 时都进行了转换,非池化内存分配器仅在分配directByteBuf 时进行了转换。个人理解时采用池化内存需要特别关注内存释放,否则为了实现池化内存预先分配的一大块内存会因为没有释放被很快分配完,造成后面没有内存进行分配。非池化分配的直接内存也需要特别注意释放,放置内存泄漏;非池化分配的heap内存(其实就是一个 byte 数组)则可以在对象被回收时同时被回收掉,发生内存泄漏的可能性较小。
本节介绍Netty中内存泄漏检测相关的类,仅做一个大致介绍,类中的重要方法我们放在后面介绍。
主要负责使用 track 方法对指定的 ByteBuf 进行内存检测泄漏进行追踪,并返回负责追踪的 ResourceLeakTracker 类实例,同时在调用 track 方法时,也会根据指定的检测级别汇报最近的内存泄漏检测结果。该类由工厂类 ResourceLeakDetectorFactory 负责实例化,默认的实现为 ResourceLeakDetector ,在 ResourceLeakDetectorFactory 类的默认实现 DefaultResourceLeakDetectorFactory 中,也会根据用户是否配置了 来决定采用默认实现 ResourceLeakDetector 还是使用用户自定义的 ResourceLeakDetector ,用户自定义的 ResourceLeakDetector 必须是其子类。
默认实现为 DefaultResourceLeak , DefaultResourceLeak 实现了 ResourceLeakTracker 和 ResourceLeak 接口,同时也继承了类 WeakReference ,是一个弱引用实现。首先,同上面 例2 的结果一样,如果在使用 ByteBuf 时忘了调用 方法,那么将不会调用 方法去手动清空该弱引用持有的实际对象,在发生GC时,会由废品收集器对弱引用持有的实际对象进行回收,即发生了内存泄漏,同时该弱引用自身也会被加入到引用队列中,该引用队列是 ResourceLeakDetector 的成员域,上面介绍 ResourceLeakDetector 类时说到该类会在用户 track 指定 ByteBuf 是汇报检测结果,该类的汇报数据来源就是引用队列。 DefaultResourceLeak 同时还提供了 record 方法可以让用户在指定时机选择调用,这个方法可以记录用户的调用轨迹(堆栈)。 Record 同时也是一种单链表,在 DefaultResourceLeak 中就使用单链表记录用户的调用轨迹。
DefaultResourceLeak 供用户记录程序调用轨迹的类,也就是 方法返回的对象,继承自 Throwable ,因此可以使用 方法获得调用轨迹信息,打印在内存泄漏报告中可以让用户更好的排除内存泄漏问题。
在上面介绍 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 类中使用了配置参数 来控制是否只是在调用增加或减少引用计数器的方法时才调用 record 方法记录调用轨迹,默认为false。 AdvancedLeakAwareCompositeByteBuf 中 retain 和 release 方法因为改变了引用计数器就直接调用了 record 方法,而该类中的其他方法则根据 的配置决定是否调用 record 方法,这里为了节省篇幅就不列出 AdvancedLeakAwareCompositeByteBuf 类中调用 record 的方法了,读者可自行查看。
在介绍相关配置参数之前,我们先看下Netty提供的内存泄漏检测级别:
和 使用的 ByteBuf 包装类都是 AdvancedLeakAwareCompositeByteBuf ,我们上面介绍 ResourceLeakDetector 类时提到该类使用 track 方法对指定的 ByteBuf 进行内存检测泄漏进行追踪,并返回负责追踪的 ResourceLeakTracker 类实例,同时在调用 track 方法时,也会根据指定的检测级别汇报最近的内存泄漏检测结果。如果内存泄漏检测级别为 时则每次调用 track 方法都会进行内存泄漏报告;如果级别为 或者 则会以一定频率进行内存泄漏报告,而不是每次 track 都进行报告。
是否关闭Netty内存泄漏检测功能,默认为false。如果该参数配置为false,则默认的内存泄漏检测级别根据此参数的配置为 ,否则默认的级别为 。
配置内存泄漏检测级别的参数,用于老版本的配置参数。
新的内存泄漏检测级别参数,如果没有配置,则会采用老版本参数配置的级别作为最终配置。
在第4节介绍内存泄漏检测相关类时,我们介绍过 DefaultResourceLeak 提供了 record 方法记录用户的调用轨迹,如果当前保存的调用轨迹记录数 Record 大于参数 配置的值,那么会以一定的概率(1/2^n)删除头结点之后再加入新的记录,当然也有可能不删除头结点直接新增新的记录。
该参数的默认为4。
上面介绍过,在 AdvancedLeakAwareCompositeByteBuf 类中使用了配置参数 来控制是否只是在调用增加或减少引用计数器的方法时才调用 record 方法记录调用轨迹,默认为false。
在介绍 ResourceLeakDetector 类时提到过,默认的 ResourceLeakDetector 类就是 ResourceLeakDetector ,但是用户可以使用参数 来决定采用默认实现 ResourceLeakDetector 还是使用用户自定义的 ResourceLeakDetector 。
我们在第二节介绍了Netty中将普通 ByteBuf 转为具有内存泄漏检测功能的 ByteBuf 是通过 方法实现的。
这里我们先看下该方法的源码:
上面的源码中是调用 (buf) 返回 ResourceLeakTracker 类对象的,这里我们看下默认的 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 。在 方法中调用的 reportLeak 输出的就是引用队列中的弱引用 DefaultResourceLeak :
到这里,已经基本上介绍完Netty内存检测的实现原理,下面我们再看下 是如何记录调用轨迹的:
最后我们再看下 Record 是如何输出调用轨迹的,前面我们说到 Record 继承自类 Throwable ,因此可使用 getStackTrace 方法获取实例化该对象时的调用轨迹,所以上面在输出内存泄漏报告时就调用了 方法:
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。