前两天,项目中发现一个Bug。我们使用的 RocketMQ ,在服务启动后会创建MQ的消费者实例,来订阅topic。测试过程中,发现服务启动一段时间后,与 RocketMQ 的连接就会断掉,从而找不到订阅关系,监听不到数据。
一、Bug的产生
经过回溯代码,发现订阅的逻辑是这样的。将 ConsumerStarter 类注册到Spring,并通过 PostConstruct 注解触发初始化方法,完成MQ消费者的创建和订阅。
上面代码中的 Subscriber 类是同事写的一个工具类,订阅的时候都调用这里。这里面也不复杂,就是调用 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 定义成 ConsumerStarter 类中的成员变量也是可以的,因为 ConsumerStarter 是注册到了 Spring 中。在Bean的生命周期内,不会被回收。
如上代码,把 subscriber 作用域提到类级别,事实证明这样也是没问题的。
还有个更优的方案是,将 Subscriber 直接注册到 Spring 中,由 PostConstruct 注解触发初始化完成对MQ的创建和订阅;由 PreDestroy 注解完成资源的释放。这样,资源的创建和销毁跟Bean的生命周期绑定,也是没问题的。
到目前为止,这个Bug的原因和解决方案都有了。但还有个问题,笔者一时没想明白。
二、疑问点
为了确定哪些对象是垃圾,在Java中使用了可达性分析的方法。
它通过通过一系列的 GC roots 对象作为起点搜索。从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
结合代码来看,虚拟机栈中引用的对象是 subscriber ,而 subscriber 对象中又包含了 Consumer 对象。 Consumer 对象是在 RocketMQ 中创建的,并且调用了它的 consumer.start 方法。
我大概看了下 RocketMQ ,作为一个 Consumer 实例,它肯定会定期从 Name Server 拉取消息;并且定时向服务器发生心跳。而且在 RocketMQ 代码中,我也看到了 ScheduledExecutorService 这种定时器的启动。
那么,这一切说明, subscriber 类的 consumer 的实例是活跃的呀,它们之间是可达的,不应该被回收吧?
这个问题也可以被描述成:如果A对象没有了引用,是确定可以被回收的 比如局部变量subscriber,方法执行完应该就被销毁 ;但是如果A对象中还有线程在活跃, 比如在活跃的线程是consumer实例 ,此时A对象还会被回收吗?
此处可能逻辑是错误的,也是笔者没能理解的地方。望大佬指正、解惑。
然后,基于上面的问题,笔者又做了两个测试。
回到上面项目中的代码,此时我还是将 Subscriber 定义成局部变量,这样在GC的时候,它还是要被回收的。在这里,可以通过 System.gc(); 来手动触发GC。
1、在Subscriber类中新建线程
在 Subscriber 类中,通过 new Thread().start(); 的方式来创建一个线程并调用它的启动方法,整体代码如下:
如果是这种情况,当触发GC的时候, Subscriber 类不会被回收, finalize 方法也没有被调用,线程还会持续输出。
2、在Subscriber类中调用其他线程类
首先定义一个线程类 MyThread1 ,它的run方法也是死循环。
然后在 Subscriber 类中通过 MyThread1 thread1 = new MyThread1(); 实例化。
然后通过 new Thread(thread1).start(); 来启动它。
此时,如果触发GC, Subscriber 类照样会被回收, finalize 方法也会被调用,但 thread1线程仍然还会持续输出。
通过这两个测试,我更不太明白了。都是在 Subscriber 类中启动新的线程,为什么结果却不同呢?
是因为在测试1中,本类的线程还未执行结束,方法未结束吗?
请大佬们带着批判的目光审视第二部分,其中逻辑可能有误,请大佬们不吝赐教。如果一两句话扯不清楚,也希望有大佬可以专门写篇文章讲讲这里面的逻辑误区~