这部分内容略显得枯燥,但是对于了解垃圾收集器的细节却是不得不学习这部分内容。

HotSpot虚拟机垃圾回收细节

然后严密的体系到达一个相对底层的位置,都会变得十分复杂,源代码和原理即是如此,我们了解垃圾回收不能够仅仅停留与表面,而是要探究如何实现这样一个复杂的功能——这对我们了解和掌握Java体系有一定的帮助

一、GC Roots枚举效率的提升

无论是哪种垃圾收集器,在枚举可达性分析的根节点时,都不得不“Stop the World”——暂停所有用户线程,才能开始枚举根节点,具体原因参考子文章《垃圾回收之Stop the World、对象消失问题》

通过了解Java虚拟机运行时数据区的知识我们了解到,GC Roots(垃圾回收根节点,可达性分析根)来自两个部分全局引用(方法区)、运行时上下文(Java栈),这两个地方虽然没有Java堆那么庞大,但是如果完全历遍搜索其中的对象位置也是十分耗时的。

为了能够在GC时准确快速找到需要的对象位置,Java虚拟机使用空间换时间,额外保存OopMap(简单对象指针记录表)记录程序执行到相应位置时,GC Roots对象的分配情况

二、安全点和安全区策略

刚才我们提到了相应位置,什么是正确的位置呢,如果为每一条语句位置都开辟一个OopMap来保存状态空间花费就太大了,因此虚拟机实现在对象变更的情况保存一个OopMap——循环末尾、方法返回前、调用后第一条语句、异常可能发生处、内存开辟处(即对象创建等)…这些有对应OopMap的位置——称为安全点

在用户线程都到达各种的安全点之后,全部中断“Stop the World”,GC Roots枚举才能开始(这也就是不能控制开始GC时刻 的原因)

那么又如何保证线程都到达安全点乖乖中断呢?又两种方法,虚拟机使用第二种:

  1. 抢占式中断:有GC请求时,直接中断全部用户线程,然后让没有在安全点的线程恢复中断,到安全点再次中断
  2. 主动式中断:线程运行到安全点,主动查询是否有GC请求,如果有,自动中断

考虑一个情况——一个线程因为某种原因(Sleep、Blocked)需要很长时间才能到达安全点,但是GC等不了那么久了,内存已经岌岌可危了。

我们发现在线程已经停止的情况下,也是不存在对象创建和更改的,这和线程中断的目的相同,为了保证GC Roots选取正确,因此,在该区域内线程对GC Roots枚举为安全——称为安全区

当线程离开安全区时,线程需要判断当前是否在GC Roots枚举,有则中断等待

三、记忆集和卡表

基于分代收集理论(经典的垃圾收集器)或基于分区收集的收集器(G1等)都需要考虑一件事,当前收集区域内的对象如果被另一区域对象引用,难道要完全把另一区域的对象也加入进入可达性分析节点吗?

要知道老年代能够选取的GC Roots数量不比新生代GC Roots少,这样相当于一次全回收(当内存告急,将全部对象都标记判断回收清理内存),明显是不讲道理的。跨代/跨区引用仅仅是少数…

这里Java虚拟机使用记忆集的手段,把其他区域划整为零,存在跨代引用的一小部分才引入分析。又根据划分的粒度——字长精度、对象精度以及卡表

卡表也就是一种记忆集的粗粒度实现,用以快速判断区域内是否有跨代引用

四、写屏障

虚拟机在赋值操作后面生成相应指令,用以更新相应卡表的行为

五、并发可达性分析的两种避免对象消失的手段

关于GC并发回收的对象消失问题——请看《垃圾回收之Stop the World、对象消失问题》

有如下两种方法解决该问题:

  1. 增量更新:在并发过程中,GC扫描过的对象被赋予一个新的未被扫描过的对象的引用时,将该被扫描标记过的对象重新加入GC Roots开始重新扫描。(使得在GC可达性标记节点突然被转移到被扫描过的对象“名下”的对象也能被可达性分析发现)
  2. 原始快照:如果一个没被扫描过的对象被删除(删除说明它此前是能够被访问到,这里删除是指指向它的变量被赋予新值)时,将其加入GC Roots扫描(原始快照故名思意,就是不管在GC标记过程中对象的修改,按照原来的可达链条分析,实际上还是修改了,但是分析结果一样)

欢迎关注我、一起共勉

⭐️⭐️代码之狐⭐️⭐️
主要内容:

  • 时不时更新算法题解,算法与数据结构
  • 时不时分享心灵鸡汤,详见杂谈栏
  • 目前主要在学Java高级内容(虚拟机、框架什么的),会将书中的知识点提炼总结分享

怎么还没人关注我😢

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐