前两天,项目中发现一个Bug。我们使用的 RocketMQ
,在服务启动后会创建MQ的消费者实例,来订阅topic。测试过程中,发现服务启动一段时间后,与 RocketMQ
的连接就会断掉,从而找不到订阅关系,监听不到数据。
经过回溯代码,发现订阅的逻辑是这样的。将 ConsumerStarter
类注册到Spring,并通过 PostConstruct
注解触发初始化方法,完成MQ消费者的创建和订阅。
上面代码中的 Subscriber
类是同事写的一个工具类,订阅的时候都调用这里。这里面也不复杂,就是调用 RocketMQ
,完成创建和订阅。
上面的代码看起来平平无奇,但实际上他重写了 finalize
方法。并且在里面执行了 consumer.shutdown()
,将 RocketMQ
断开了,这里是诱因。
finalize
是 Object
中的方法。在GC(垃圾回收器)决定回收一个不被其他对象引用的对象时调用。子类覆写 finalize
方法来处置系统资源或是负责清除操作。
回到项目中,他这样的写法就是在 Subscriber
类被回收的时候,断开 RokcketMQ
的连接,因而产生了Bug。最简单的方式就是把 shutdown
这句代码删掉,但这似乎不是好的解决方案。
在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。
在 Subscriber
类中,通过 new Thread().start();
的方式来创建一个线程并调用它的启动方法,整体代码如下:
如果是这种情况,当触发GC的时候, Subscriber
类不会被回收, finalize
方法也没有被调用,线程还会持续输出。
首先定义一个线程类 MyThread1
,它的run方法也是死循环。
然后在 Subscriber
类中通过 MyThread1 thread1 = new MyThread1();
实例化。
然后通过 new Thread(thread1).start();
来启动它。
此时,如果触发GC, Subscriber
类照样会被回收, finalize
方法也会被调用,但 thread1
线程仍然还会持续输出。
通过这两个测试,我更不太明白了。都是在 Subscriber
类中启动新的线程,为什么结果却不同呢?
是因为在测试1中,本类的线程还未执行结束,方法未结束吗?