内容简介:前两天,项目中发现一个Bug。我们使用的经过回溯代码,发现订阅的逻辑是这样的。将上面代码中的
前两天,项目中发现一个Bug。我们使用的 RocketMQ
,在服务启动后会创建 MQ
的消费者实例。测试过程中,发现服务启动一段时间后,与 RocketMQ
的连接就会断掉,从而找不到订阅关系,监听不到数据。
一、Bug的产生
经过回溯代码,发现订阅的逻辑是这样的。将 ConsumerStarter
类注册到Spring,并通过 PostConstruct
注解触发初始化方法,完成 MQ
消费者的创建和订阅。
上面代码中的 Subscriber
类是同事写的一个 工具 类,封装了一些连接 RocketMQ
的配置信息,使用的时候都调用这里。这里面也不复杂,就是连接 RocketMQ
,完成创建和订阅。
1、finalize
上面的代码看起来平平无奇,但实际上他重写了 finalize
方法。并且在里面执行了 consumer.shutdown()
,将 RocketMQ
断开了,这里是诱因。
finalize
是 Object
中的方法。在GC(垃圾回收器)决定回收一个不被其他对象引用的对象时调用。子类覆写 finalize
方法来处置系统资源或是负责清除操作。
回到项目中,他这样的写法就是在 Subscriber
类被回收的时候,断开 RokcketMQ
的连接,因而产生了Bug。最简单的方式就是把 shutdown
这句代码删掉,但这似乎不是好的解决方案。
2、为何被回收?
在Java的内存模型中,有一个 虚拟机栈
,它是线程私有的。
虚拟机栈是线程私有的,每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈,虚拟机栈表示Java方法执行的内存模型,每调用一个方法就会为每个方法生成一个栈帧(Stack Frame),用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。虚拟机栈的生命周期和线程是相同的
在上面的 ConsumerStarter.init()
方法中, Subscriber subscriber = new Subscriber()
被定义成了局部变量,在方法执行完毕后, subscriber
变量就没有了引用,然后GC掉。
很快,我就有了新的想法,将 Subscriber
定义成 ConsumerStarter
类中的成员变量也是可以的,因为 ConsumerStarter
是注册到了 Spring
中。在Bean的生命周期内,不会被回收。
如上代码,把 subscriber
变量作用域提到类级别,事实证明这样也是没问题的。
还有个更优的方案是,将 Subscriber
类直接注册到 Spring
中,由 PostConstruct
注解触发初始化完成对MQ的创建和订阅;由 PreDestroy
注解完成资源的释放。这样,资源的创建和销毁跟Bean的生命周期绑定,也是没问题的。
到目前为止,这个Bug的原因和解决方案都有了。但还有个问题,笔者当时也没想明白。
二、线程与垃圾回收
为了确定哪些对象是垃圾,在Java中使用了可达性分析的方法。
它通过通过一系列的 GC roots
对象作为起点搜索。从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在上面的例子中,虽然 Subscriber
类已经不被引用,要被回收,但是它里面还有 RocketMQ
的 Consumer
实例还在以线程的方式运行,这种情况下, Subscriber
类也会被回收掉吗?
那么,就变成了这样一个问题: 如果一个对象A已不被引用,符合GC条件;但是它里面还有一个线程对象a1在活跃,那么此时这个对象A还会被回收吗?
为了验证这个问题,笔者做了一个测试。
1、测试一
在这里,还是以 Subscriber
类为例,给它启动一个线程,看是否还会被GC。简化代码如下:
测试流程是: 启动服务 > Subscriber类被创建 > 线程执行10次循环 > 手动调用GC
得出结果如下:
15:01:07.110 [main] INFO - Subscriber已启动... 15:01:07.112 [Thread-9] INFO - 执行线程次数:0 15:01:07.357 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor' 15:01:07.568 [main] INFO - Starting ProtocolHandler ["http-nio-8081"] 15:01:07.609 [main] INFO - Tomcat started on port(s): 8081 (http) with context path '' 15:01:07.614 [main] INFO - Started BootMybatisApplication in 3.654 seconds 15:01:08.114 [Thread-9] INFO - 执行线程次数:1 15:01:09.114 [Thread-9] INFO - 执行线程次数:2 15:01:09.302 [http-nio-8081-exec-2] INFO - 调用GC。。。 15:01:10.122 [Thread-9] INFO - 执行线程次数:3 15:01:11.123 [Thread-9] INFO - 执行线程次数:4 15:01:12.123 [Thread-9] INFO - 执行线程次数:5 15:01:12.319 [http-nio-8081-exec-3] INFO - 调用GC。。。 15:01:13.123 [Thread-9] INFO - 执行线程次数:6 15:01:14.124 [Thread-9] INFO - 执行线程次数:7 15:01:15.125 [Thread-9] INFO - 执行线程次数:8 15:01:15.319 [http-nio-8081-exec-5] INFO - 调用GC。。。 15:01:16.125 [Thread-9] INFO - 执行线程次数:9 15:01:17.775 [http-nio-8081-exec-7] INFO - 调用GC。。。 15:01:17.847 [Finalizer] INFO - finalize-------------Subscriber对象被销毁 复制代码
从结果上看,如果 Subscriber
类如果有活跃的线程在运行,它是不会被回收的;等线程运行完之后,再次调用GC,才会被回收掉。 不过,先不要急着下结论,我们再测试一下别的情况。
2、测试二
这次,我们先创建一个线程类 Thread1
。
它的run方法,我们跟上面保持一致。然后在 Subscriber
对象中通过线程调用它。
流程和测试1一样,这次的结果输出如下:
14:59:20.193 [main] INFO - Subscriber已启动... 14:59:20.194 [Thread-9] INFO - 执行次数:0 14:59:20.359 [Finalizer] INFO - finalize-------------Subscriber对象被销毁 14:59:20.444 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor' 14:59:20.699 [main] INFO - Starting ProtocolHandler ["http-nio-8081"] 14:59:20.745 [main] INFO - Tomcat started on port(s): 8081 (http) with context path '' 14:59:20.751 [main] INFO - Started BootMybatisApplication in 3.453 seconds 14:59:21.197 [Thread-9] INFO - 执行次数:1 14:59:22.198 [Thread-9] INFO - 执行次数:2 14:59:23.198 [Thread-9] INFO - 执行次数:3 14:59:24.198 [Thread-9] INFO - 执行次数:4 14:59:25.198 [Thread-9] INFO - 执行次数:5 14:59:26.198 [Thread-9] INFO - 执行次数:6 14:59:27.198 [Thread-9] INFO - 执行次数:7 14:59:28.199 [Thread-9] INFO - 执行次数:8 14:59:29.199 [Thread-9] INFO - 执行次数:9 复制代码
从结果上看, Subscriber
创建之后就因为 ConsumerStarter.init()
方法执行完毕,而被销毁了,丝毫没有受线程的影响。
咦,这就有意思了。从逻辑上看,两个测试都是通过 new Thread()
开启了新的线程,为啥结果却不一样呢?
当即就在微信联系了 芋道源码 大神,大神果然老道,他一眼指出:你这个应该是内部类的问题。
3、匿名内部类
我们把目光回到测试1的代码中。
如果在方法中,这样创建一个线程,实际上创建了一个匿名内部类。但是,它有什么特殊之处吗?竟然会阻止对象会正常回收。
我们知道在内部类编译成功后,它会产生一个class文件,该class文件与外部类并不是同一class文件。在上面的代码中,编译后就产生一个class文件叫做: Subscriber$1.class
。
通过反编译软件,我们可以得到这个class文件的内容如下:
看到这里,就已经很明确了。这个内部类虽然与外部类不是同一个class文件,但是它保留了对外部类的引用,就是这个 Subscriber this$0
。
只要内部类的方法执行不完,就会还保留外部类的引用实例,所以外部类就不会被GC回收。
所以,再回到一开始的问题:
如果一个对象A已不被引用,符合GC条件;但是它里面还有一个线程对象a1在活跃,那么此时这个对象A还会被回收吗?
答案也是肯定的,除非线程对象a1还在引用A对象。
三、匿名内部类与垃圾回收
第二部分中,我们看的是匿名内部类中开启线程和垃圾回收的关系,我们再看看与线程无关的情况。
在意识到是因为匿名内部类导致垃圾回收的问题时,在网上找到一篇文章。说的也是匿名内部类垃圾回收的问题,原文链接:匿名内部类相关的gc
里面的代码笔者也测试过了,确实也如原文作者所说:
从结果推测,匿名内部类会关联对象,内部类对象不回收,导致主对象无法回收;
但实际上, 这并不是匿名内部类的问题,或者说不完全是它的问题,而是 static
修饰符的问题。
1、测试一
使用匿名内部类我们必须要继承一个父类或者实现一个接口,所以我们随便创建一个接口。
public interface TestInterface { void out(); } 复制代码
然后在 Subscriber
外部类中使用它。
然后启动服务,输出结果如下:
16:37:53.626 [main] INFO - Root WebApplicationContext: initialization completed in 1867 ms 16:37:53.710 [main] INFO - Subscriber已启动... 16:37:53.711 [main] INFO - 匿名内部类测试... 16:37:53.866 [Finalizer] INFO - finalize-------------Subscriber对象被销毁 16:37:53.950 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor' 16:37:54.180 [main] INFO - Starting ProtocolHandler ["http-nio-8081"] 16:37:54.224 [main] INFO - Tomcat started on port(s): 8081 (http) with context path '' 复制代码
从结果来看, Subscriber
对象被创建,使用完之后,不需要手动调用 GC
,就被销毁了。
如果将 interface1
变量定义成静态的呢? static TestInterface interface1;
输出结果如下:
16:43:20.331 [main] INFO - Root WebApplicationContext: initialization completed in 1826 ms 16:43:20.404 [main] INFO - Subscriber已启动... 16:43:20.405 [main] INFO - 匿名内部类测试... 16:43:20.673 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor' 16:43:20.955 [main] INFO - Starting ProtocolHandler ["http-nio-8081"] 16:43:21.002 [main] INFO - Tomcat started on port(s): 8081 (http) with context path '' 16:43:21.007 [main] INFO - Started BootMybatisApplication in 3.327 seconds 16:43:33.095 [http-nio-8081-exec-1] INFO - Initializing Servlet 'dispatcherServlet' 16:43:33.102 [http-nio-8081-exec-1] INFO - Completed initialization in 7 ms 16:43:33.140 [http-nio-8081-exec-1] INFO - 调用GC。。。 16:43:35.763 [http-nio-8081-exec-2] INFO - 调用GC。。。 复制代码
如果把 interface1
定义成静态的,再怎么调用 GC
, Subscriber
都不会被回收掉。
怎么办呢?可以在 interface1
使用完之后,把它重新赋值成空。 interface1=null;
,这时候, Subscriber
类又会像测试1里面那样,被正常回收掉。
不过,这又是为什么呢?Java的垃圾回收是依靠 GC Roots
开始向下搜索的,当一个对象到 GC Roots
没有任何引用链相连时,则证明此对象是不可用的;那么反之,就是可用的,不可回收的。
GC Roots
包含以下对象:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI[即一般说的Native]引用的对象
也就是说,如果我们把匿名内部类 interface1
定义成静态的对象,它就会当作 GC Root
对象存在,此时,这个内部类里面又保持着外部类 Subscriber
的引用,所以迟迟不能被回收。
2、测试二
为什么说, 这并不是匿名内部类的问题,或者说不完全是它的问题,而是static修饰符的问题 ,我们可以再做一个测试。
关于匿名内部类,我们先不管它是如何产生的,也不理会又是怎么用的,我们只记住它的特性之一:
它会保持外部类的引用。
所以,我们先摒弃什么劳什子内部类,就创建一个普通的类,让它保持调用类的引用。
然后在外部类 Subscriber
中,创建它的实例,并调用:
最后启动服务,输出结果如下:
17:30:02.331 [main] INFO - Root WebApplicationContext: initialization completed in 1726 ms 17:30:02.443 [main] INFO - Subscriber已启动... com.viewscenes.netsupervisor.controller.test.Subscriber@7ea899a9 17:30:02.447 [main] INFO - User对象输出信息.... com.viewscenes.netsupervisor.controller.test.Subscriber@7ea899a9 17:30:02.605 [Finalizer] INFO - finalize-------------Subscriber对象被销毁 17:30:02.606 [Finalizer] INFO - finalize-------------User对象被销毁 17:30:02.676 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor' 17:30:02.880 [main] INFO - Starting ProtocolHandler ["http-nio-8081"] 复制代码
这种情况下,都是会正常被回收的。
但如果这时候再把 user
对象定义成静态属性,不管怎么 GC
,还是不能回收。
由此看来,可以得出这样一个结论:
- 匿名内部类并不会妨碍外部类的正常GC,而是不能将它定义成静态属性引用。
- 静态匿名内部类,导致外部类不能正常回收的原因就是:它作为GC Root对象却保持着外部类的引用。
四、总结
本文通过一个小Bug,引发了内存模型和垃圾回收的问题。因为基础理论知识还不过硬,只能通过不同的测试,来解释整个事件的原因。
另外,虽然Java帮我们自动回收垃圾,但大家写代码的时候注意不要引发内存泄露哦。
以上所述就是小编给大家介绍的《由一个Bug来看Java内存模型和垃圾回收》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- JVM内存模型与垃圾回收
- JVM内存模型和垃圾回收机制
- 【求解惑】由一个Bug来看Java内存模型和垃圾回收
- 垃圾回收算法(7)-分代回收算法
- JAVA 垃圾回收机制(二) --- GC回收具体实现
- 对象回收判定与垃圾回收算法-JVM学习笔记(1)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
算法的陷阱
阿里尔•扎拉奇 (Ariel Ezrachi)、莫里斯•E. 斯图克 (Maurice E. Stucke) / 余潇 / 中信出版社 / 2018-5-1 / CNY 69.00
互联网的存在令追求物美价廉的消费者与来自世界各地的商品只有轻点几下鼠标的距离。这诚然是一个伟大的科技进步,但却也是一个发人深思的商业现象。本书中,作者扎拉奇与斯图克将引领我们对由应用程序支持的互联网商务做出更深入的检视。虽然从表面上看来,消费者确是互联网商务兴盛繁荣过程中的获益者,可精妙的算法与数据运算同样也改变了市场竞争的本质,并且这种改变也非总能带来积极意义。 首当其冲地,危机潜伏于计算......一起来看看 《算法的陷阱》 这本书的介绍吧!