聊聊Java中引用类型-引用类型应用与内存泄漏

六点A君

共 1344字,需浏览 3分钟

 · 2022-01-04

本文将接着上一篇文章内容,聊聊Java中引用使用以及可能产生的内存泄漏。

Java程序员是幸福的,不用过多考虑内存申请和释放,Jvm在Java与C++之间构建一堵由内存动态分配和垃圾收集技术所围成的高墙,是的Java程序员能全身心投入到实际开发当中,是否会有墙外面人想进去,墙里面的人却想出来呢?

内存溢出和内存泄漏:

  • 内存溢出:俗称OOM,指JVM无法申请到足够内存空间或者GC失败,而抛出的Error,OOM造成的后果十分严重,使得应用无法对外提供服务。

  • 内存泄漏:部分内存已经没有用了,但是却没有被回收,对于Java而言,就是GC无法回收这部分本应该被回收的内存。

Obeject 的 finalize

Object 的finalize方法,当对象即将被回收时,可以被执行finalize方法,但是并不能依赖这个方法来清除,否则将造成内存泄漏。例如当进行socket编程时,需要在finally块中执行close方法,从而释放资源,如果忘记释放了,则可能会造成内存泄漏,例如在 重写了finalize方法:

  1. /**

  2. * Cleans up if the user forgets to close it.

  3. */

  4. protectedvoid finalize()throwsIOException{

  5. close();

  6. }

方法注释也很简洁明了,所以在释放这一类工具类时,一定要手动执行close方法,而不是交给finalize去替我们释放,这样容易引发内存泄漏。

ThreadLocal

ThreadLocal很常见,原理不难理解,在Thread类中有两个ThreadLocalMap变量,维护着多个本地线程变量:Thread:

  1. /* ThreadLocal values pertaining to this thread. This map is maintained

  2. * by the ThreadLocal class. */

  3. ThreadLocal.ThreadLocalMap threadLocals =null;


  4. /*

  5. * InheritableThreadLocal values pertaining to this thread. This map is

  6. * maintained by the InheritableThreadLocal class.

  7. */

  8. ThreadLocal.ThreadLocalMap inheritableThreadLocals =null;

ThreadLocalMap结构:

  1. privatestaticfinalint INITIAL_CAPACITY =16;

  2. privateEntry[] table;// 存放元素的数组

  3. privateint size =0;// 大小

  4. privateint threshold;// Default to 0

其Entry为一个WeakReference子类,reference为对应的ThreadLocal 实例,即如果没有其他引用,就会被回收:

  1. staticclassEntryextendsWeakReference<ThreadLocal>{

  2. /** The value associated with this ThreadLocal. */

  3. Object value;


  4. Entry(ThreadLocal k,Object v){

  5. super(k);

  6. value = v;

  7. }

  8. }

Entry 由于没有使用引用队列,故一旦没有引用,则会直接变为inactive状态,从而被gc回收。但是另一方面,ThreadLocalMap 中 Entry[] 为一个引用,所以事实上,就算ThreadLocal没有其他地方使用,也会被Thread引用,所以只要Thread不销毁,Entry 并不会因WeakReference特性而销毁。另一方面,由于Entry 中key(ThreadLocal)是弱引用类型,所以一旦ThreadLocal没有被引用,那么ThreadLocal将会在下次gc被回收,造成的效果为:

  1. Entry 不为null。

  2. Entry实例的get方法获取为null,因为ThreadLocal已经被回收了,但是value仍然存在,这就造成了泄漏。

在ThreadLocalMap的set操作过程,虽然在检查到上述第二种情况,并且会尝试将数组内所有满足该情况的元素节点的value都设置为null。这也是为啥追求极致性能的netty会自己造一个FastThreadLocal来取代TheadLocal:https://blog.csdn.net/anLA_/article/details/110777608所以在使用ThreadLocal时,在用完时,一定要执行remove方法,清除对应引用。

Netty中内存泄漏检测

netty中通过BufAllocator使用ByteBuf时,都会包装一层校验内存泄漏的逻辑:

  1. protectedstaticByteBuf toLeakAwareBuffer(ByteBuf buf){

  2. ResourceLeakTracker<ByteBuf> leak;

  3. switch(ResourceLeakDetector.getLevel()){

  4. case SIMPLE:

  5. leak =AbstractByteBuf.leakDetector.track(buf);

  6. if(leak !=null){

  7. buf =newSimpleLeakAwareByteBuf(buf, leak);

  8. }

  9. break;

  10. case ADVANCED:

  11. case PARANOID:

  12. leak =AbstractByteBuf.leakDetector.track(buf);

  13. if(leak !=null){

  14. buf =newAdvancedLeakAwareByteBuf(buf, leak);

  15. }

  16. break;

  17. default:

  18. break;

  19. }

  20. return buf;

  21. }

上述会将ByteBuf封装一层 DefaultResourceLeak,而 DefaultResourceLeak 则是一个WeakReference对象:

  1. DefaultResourceLeak(

  2. Object referent,

  3. ReferenceQueue<Object> refQueue,

  4. Set<DefaultResourceLeak> allLeaks){

  5. super(referent, refQueue);


  6. assert referent !=null;


  7. // Store the hash of the tracked object to later assert it in the close(...) method.

  8. // It's important that we not store a reference to the referent as this would disallow it from

  9. // be collected via the WeakReference.

  10. trackedHash =System.identityHashCode(referent);

  11. allLeaks.add(this);

  12. // Create a new Record so we always have the creation stacktrace included.

  13. headUpdater.set(this,newRecord(Record.BOTTOM));

  14. this.allLeaks = allLeaks;

  15. }

在应用中,对返回的ByteBuf对象进行操作时,都会间接调用 ResourceLeakDetectorreportLeak 方法:

  1. privatevoid reportLeak(){

  2. if(!logger.isErrorEnabled()){

  3. clearRefQueue();

  4. return;

  5. }

  6. // Detect and report previous leaks.

  7. for(;;){

  8. @SuppressWarnings("unchecked")

  9. DefaultResourceLeakref=(DefaultResourceLeak) refQueue.poll();// 如果有弱引用被回收,则会进入队列

  10. if(ref==null){

  11. break;

  12. }

  13. if(!ref.dispose()){// 如果已经释放,则不是泄漏

  14. continue;

  15. }

  16. String records =ref.toString();

  17. if(reportedLeaks.putIfAbsent(records,Boolean.TRUE)==null){

  18. if(records.isEmpty()){

  19. reportUntracedLeak(resourceType);

  20. }else{

  21. reportTracedLeak(resourceType, records);

  22. }

  23. }

  24. }

  25. }

上述方法有以下要点:

  1. 如果进入refQueue,则说明有弱引用被回收,如果dispose了,则说明回收之前执行release,释放了资源,否则没有释放。

  2. 报告泄漏最近调用路径

使用MAT进行堆dump分析

Java引用类型,还有一个用途点,就是在使用eclipse mat工具时,由于Java对象通过引用链接,所以当找到一个大对象时,可以过滤其他引用类型,直接选择强引用,从而一步一步最终拿到当时调用链路栈:b949017815da1a944f762d15f6ac3d19.webp这样就能轻而易举解决多数OOM问题的根源。

总结

  1. Java引用类型很强大,可以来实现一些高校的应用内缓存,可以监听gc动作等。

  2. 根据部分类文档及结合代码来确定 使用时要注意是否需要手动释放。

觉得对你有帮助?不如关注博主公众号: 六点A君


浏览 55
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报