垃圾收集相关知识

2022-01-03  本文已影响0人  DH大黄

垃圾收集相关知识

思维导图

JVM垃圾收集思维导图.jpg

回收的对象

堆,方法区(方法区虚拟机不要求实现)

如何判断一个对象可以回收

引用计数算法

主流的Java虚拟机没有使用该算法。因为简单的引用计数无法解决循环引用问题,需要很多额外的操作

可达性分析算法

GC ROOT 到该对象是否可达

能够作为GC ROOT的对象

关于GC Roots根结点枚举的一个优化

首先要先明确一个前提:虚拟机(就算是几乎不会发生停顿的CMS、G1、ZGC等收集器)在进行根结点枚举的时候,都是需要STW的。因为根结点枚举始终要在一个能够保障一致性的快照中才能进行的(整个枚举过程中子系统不会再出现根结点集合的对象引用关系的变化)

可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如 栈帧中的本地变量表)中。但是尽管我们目标明确,但是查找过程要做到高效并不是一件容易的事情。因为随着Java应用越来越大,光是方法去的大小就常有数百上千兆,每次都从这边开始查找,无疑是一个耗时的操作。

此时就用到了个OopMap来记录对象引用(这样就不需要每次都从方法区开始找了)

OopMap在类加载动作完成之后,HotSpot就可以把对象内什么偏移量上是什么类型的数据给计算出来

安全点

但是要知道,不是每条指令都生成对应的OopMap,只在特定位置生成对应的OopMap,这些位置就被称为安全点(SafePoint)(安全点太少会导致收集器等待时间过程,太多又回增大运行时的内存负荷)

可以作为安全点的几个地方:方法调用、循环跳转、异常跳转可以作为安全点

安全点=>具有让程序长时间执行的特征=>最明显的特征就是指令序列的复用,即上述的几个安全点

但是在我们进行垃圾回收,并不是所有的线程都会处于安全点。此时有两种方法可以解决这个问题

安全区域

刚刚上述说到的,都是在工作中的线程。但是正常运行的JVM虚拟机,肯定不止这种情况(Running)的线程,还有处于Sleep或者Blocked状态的线程,他们是无法响应虚拟机的中断请求的,无法走到安全点去挂起自己。此时就引入了个安全区域的概念

用户线程进入安全区域时,就会标识自己进入安全区域,那么在此期间虚拟机发起垃圾收集时,就不会去管这些已经声明自己在安全区域中的线程要离开时,会检查是否完成根结点的枚举,如果未完成则会一直等待,直到收到可以离开安全区域的信号为止

同时关于可达性算法,这里提一个概念,三色标记法

三色标记法

三色标记法的两个问题

问题的前提:用户的线程与收集器并发的工作,在标记的时候,用户的线程也会去修改引用关系。会出现两个问题

第二种问题发生的原因:

三色标记法产生的问题.png

产生上述问题需要两个前提条件

增量更新(破坏第一个条件):(CMS用到了)

当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。

这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。

原始快照(破坏第二个条件):(G1用到了)

当灰色对象要删除指向白色对象的引用关系时,就将这个要删 除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。

这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

即:将删除引用的白色对象作为根,重新扫描,保证当前白色对象不会被误删。不好的地方就是这个白色对象如果没有再被引用,也得等到下次垃圾收集时被回收(ps:这一块仅是自己的理解,后续还需要求证)

分代收集理论

因为将Java堆划分出不同的区域,所以才会有垃圾收集器每次只回收其中一个或某些部分的区域

为什么需要分代收集

如果一个区域内的大多数对象是朝生夕灭,难以熬过垃圾收集过程

那么将他们集中在一起,每次只需要考虑需要保留的少量存活对象而不是去标记那些大量需要回收的对象,

就可以以较低的成本回收大量的空间

同时,将那些难以回收的对象统一放在一个区域中,就可以以较低的频率去回收这块区域,

兼顾了垃圾收集的时间开销和内存空间的有效利用

相关名词

Partial GC(部分收集)才有了 Minor GC/Young GC(新生代收集)、Major GC/Old GC(老年代收集)、Full GC(整堆收集)、Mixed GC(收集整个新生代以及部分老年代,只有G1收集器有这样的行为)

也才能发展出 “标记-复制算法”,“标记-清除算法”,“标记-整理算法”

跨代引用问题

新生代的对象可能会被老年代引用,老年代的对象也有可能会被新生代引用

假如此时我们要回收新生代的对象,就需要去扫描整个老年代,这无疑是一个不合理的操作

此时会在新生代上建立一个全局的数据结构(记忆集 Remembered Set),将老年代划分成若干小块,标记处老年代的那一块内存存在跨代引用。然后在MinorGC的时候,只需要把包含了跨代引用的小块内存里面的对象假如到GC Roots进行扫描即可

关于记忆集,存在三种精度

目前我们用到的最多的一种就是卡表。卡表(Card Table)是卡精度的一种实现方式

卡表中的么一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作“卡页”(Card Page)

一个卡页的内存中通常不止一个对象,只要一个卡页内有一个(或多个)对象存在跨代指针,则就在对应卡表的数组元素的值标识位1,代表这个元素变脏。(GC时只需要筛选出卡表中变脏的元素,然后将其加入到GC Roots中一并扫描)

此处HotSpot使用写屏障来维护卡表。(可以使用类比的思想,AOP的Around来看待写屏障维护卡表的操作)在引用对象的赋值会产生一个环绕(Around)通知。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直 至G1收集器出现之前,其他收集器都只用到了写后屏障。

垃圾回收算法

是否移动对象都存在弊端,移动则是在回收对象时复杂,不移动则是在内存分配时复杂。相比来说,内存分配和访问的频率会比回收高很多

上一篇 下一篇

猜你喜欢

热点阅读