转载

【求解惑】由一个Bug来看Java内存模型和垃圾回收

前两天,项目中发现一个Bug。我们使用的 RocketMQ ,在服务启动后会创建MQ的消费者实例,来订阅topic。测试过程中,发现服务启动一段时间后,与 RocketMQ 的连接就会断掉,从而找不到订阅关系,监听不到数据。

一、Bug的产生

经过回溯代码,发现订阅的逻辑是这样的。将 ConsumerStarter 类注册到Spring,并通过 PostConstruct 注解触发初始化方法,完成MQ消费者的创建和订阅。

【求解惑】由一个Bug来看Java内存模型和垃圾回收

上面代码中的 Subscriber 类是同事写的一个工具类,订阅的时候都调用这里。这里面也不复杂,就是调用 RocketMQ ,完成创建和订阅。

【求解惑】由一个Bug来看Java内存模型和垃圾回收

1、finalize

上面的代码看起来平平无奇,但实际上他重写了 finalize 方法。并且在里面执行了 consumer.shutdown() ,将 RocketMQ 断开了,这里是诱因。

finalizeObject 中的方法。在GC(垃圾回收器)决定回收一个不被其他对象引用的对象时调用。子类覆写 finalize 方法来处置系统资源或是负责清除操作。

回到项目中,他这样的写法就是在 Subscriber 类被回收的时候,断开 RokcketMQ 的连接,因而产生了Bug。最简单的方式就是把 shutdown 这句代码删掉,但这似乎不是好的解决方案。

2、为何被回收

在Java的内存模型中,有一个 虚拟机栈 ,它是线程私有的。

虚拟机栈是线程私有的,每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈,虚拟机栈表示Java方法执行的内存模型,每调用一个方法就会为每个方法生成一个栈帧(Stack Frame),用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。虚拟机栈的生命周期和线程是相同的

在上面的 ConsumerStarter.init() 方法中, Subscriber subscriber = new Subscriber() 被定义成了局部变量,在方法执行完毕后,变量就没有了引用,会被销毁。

很快,我就有了新的想法,将 Subscriber 定义成 ConsumerStarter 类中的成员变量也是可以的,因为 ConsumerStarter 是注册到了 Spring 中。在Bean的生命周期内,不会被回收。

【求解惑】由一个Bug来看Java内存模型和垃圾回收

如上代码,把 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(); 的方式来创建一个线程并调用它的启动方法,整体代码如下:

【求解惑】由一个Bug来看Java内存模型和垃圾回收

如果是这种情况,当触发GC的时候, Subscriber 类不会被回收, finalize 方法也没有被调用,线程还会持续输出。

2、在Subscriber类中调用其他线程类

首先定义一个线程类 MyThread1 ,它的run方法也是死循环。

【求解惑】由一个Bug来看Java内存模型和垃圾回收

然后在 Subscriber 类中通过 MyThread1 thread1 = new MyThread1(); 实例化。

然后通过 new Thread(thread1).start(); 来启动它。

此时,如果触发GC, Subscriber 类照样会被回收, finalize 方法也会被调用,但 thread1 线程仍然还会持续输出。

通过这两个测试,我更不太明白了。都是在 Subscriber 类中启动新的线程,为什么结果却不同呢?

是因为在测试1中,本类的线程还未执行结束,方法未结束吗?

请大佬们带着批判的目光审视第二部分,其中逻辑可能有误,请大佬们不吝赐教。如果一两句话扯不清楚,也希望有大佬可以专门写篇文章讲讲这里面的逻辑误区~

原文  https://juejin.im/post/5ce4e45af265da1b70047f5f
正文到此结束
Loading...