内容简介:前两天,项目中发现一个Bug。我们使用的经过回溯代码,发现订阅的逻辑是这样的。将上面代码中的
前两天,项目中发现一个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中,本类的线程还未执行结束,方法未结束吗?
请大佬们带着批判的目光审视第二部分,其中逻辑可能有误,请大佬们不吝赐教。如果一两句话扯不清楚,也希望有大佬可以专门写篇文章讲讲这里面的逻辑误区~
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 答疑解惑之nginx
- 移动端H5解惑-页面适配(二)
- 移动端H5解惑-概念术语(一)
- 解惑3:时间频度,算法时间复杂度
- 解惑:这个 Spark 任务是数据倾斜了吗?
- Serverless 解惑——函数计算如何访问 PostgreSQL 数据库
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
细节决定交互设计的成败
张亮 / 2009-3 / 49.00元
《细节决定交互设计的成败》是一本非常实用的有关软件界面的交互设计和可用性设计方面知识的书籍,通过采用一问一答的形式,你将会有针对性地学习到一些能够很快应用在自己软件开发工作中的细节知识和诀窍。例如,如何减轻用户的等待感,如何预防和减少用户的使用错误等。另外,你会发现阅读《细节决定交互设计的成败》时会非常轻松和愉悦;这是由于《细节决定交互设计的成败》写作上的两个特点:第一,采用较多日常生活中的例子来......一起来看看 《细节决定交互设计的成败》 这本书的介绍吧!