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

栏目: Java · 发布时间: 5年前

内容简介:前两天,项目中发现一个Bug。我们使用的经过回溯代码,发现订阅的逻辑是这样的。将上面代码中的

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

一、Bug的产生

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

【求解惑】由一个Bug来看 <a href='https://www.codercto.com/topics/22013.html'>Java</a> 内存模型和垃圾回收

上面代码中的 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中,本类的线程还未执行结束,方法未结束吗?

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web程序设计

Web程序设计

塞巴斯塔 / 2008-6 / 68.00元

《Web程序设计(第4版)》是最新版,介绍了Internet和万维网的起源及演变过程,全面系统地讨论了Web开发相关的主要编程语言和工具,以及这些语言和工具之间的相互影响及优劣势。该书对全书内容进行了很多修订,并新增加了关于Ruby、Rails和Ajax的3个章节。一起来看看 《Web程序设计》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具