垃圾收集相关知识
思维导图
回收的对象
堆,方法区(方法区虚拟机不要求实现)
如何判断一个对象可以回收
引用计数算法
主流的Java虚拟机没有使用该算法。因为简单的引用计数无法解决循环引用问题,需要很多额外的操作
可达性分析算法
GC ROOT 到该对象是否可达
能够作为GC ROOT的对象
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- 同时还会有一些其他对象被“临时性”的加入
关于GC Roots根结点枚举的一个优化
首先要先明确一个前提:虚拟机(就算是几乎不会发生停顿的CMS、G1、ZGC等收集器)在进行根结点枚举的时候,都是需要STW的。因为根结点枚举始终要在一个能够保障一致性的快照中才能进行的(整个枚举过程中子系统不会再出现根结点集合的对象引用关系的变化)
可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如 栈帧中的本地变量表)中。但是尽管我们目标明确,但是查找过程要做到高效并不是一件容易的事情。因为随着Java应用越来越大,光是方法去的大小就常有数百上千兆,每次都从这边开始查找,无疑是一个耗时的操作。
此时就用到了个OopMap来记录对象引用(这样就不需要每次都从方法区开始找了)
OopMap在类加载动作完成之后,HotSpot就可以把对象内什么偏移量上是什么类型的数据给计算出来
安全点
但是要知道,不是每条指令都生成对应的OopMap,只在特定位置生成对应的OopMap,这些位置就被称为安全点(SafePoint)(安全点太少会导致收集器等待时间过程,太多又回增大运行时的内存负荷)
可以作为安全点的几个地方:方法调用、循环跳转、异常跳转可以作为安全点
安全点=>具有让程序长时间执行的特征=>最明显的特征就是指令序列的复用,即上述的几个安全点
但是在我们进行垃圾回收,并不是所有的线程都会处于安全点。此时有两种方法可以解决这个问题
- 抢先式中断:不需要线程执行代码配合,在GC时,系统让所有的用户线程全部中断如果线程没有到安全点,则恢复线程执行,知道线程执行到安全点目前几乎没有虚拟机使用这种方式
- 主动式中断:虚拟机会设置一个标志位,每个线程在执行过程中会不断地主动去轮询这个标识位一旦标志位true,线程就会在自己最近的安全点主动中断挂起
- 由于轮询标识经常出现,需要考虑指令的高效。HotSpot使用内存保护陷阱的方式,当需要暂停用户线程时,就将0x160100的内存页设置为不可读
安全区域
刚刚上述说到的,都是在工作中的线程。但是正常运行的JVM虚拟机,肯定不止这种情况(Running)的线程,还有处于Sleep或者Blocked状态的线程,他们是无法响应虚拟机的中断请求的,无法走到安全点去挂起自己。此时就引入了个安全区域的概念
用户线程进入安全区域时,就会标识自己进入安全区域,那么在此期间虚拟机发起垃圾收集时,就不会去管这些已经声明自己在安全区域中的线程要离开时,会检查是否完成根结点的枚举,如果未完成则会一直等待,直到收到可以离开安全区域的信号为止
同时关于可达性算法,这里提一个概念,三色标记法
三色标记法
- 白色:对象未被垃圾收集器访问过
- 黑色:对象已被垃圾收集器访问过,且这个对象的所有引用都已经扫描过,它是安全存活的如果有其他对象引用指向了黑色对象,无须重新扫描一遍
- 灰色:标识对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过
三色标记法的两个问题
问题的前提:用户的线程与收集器并发的工作,在标记的时候,用户的线程也会去修改引用关系。会出现两个问题
- 原本应该消亡的对象,被错误的标记为存活。(这个问题可以容忍,本身发生的概率不高,下次垃圾回收时再回收就可以了)
- 原本应该存活的对象被标记为消亡(这就不能容忍了)
第二种问题发生的原因:
产生上述问题需要两个前提条件
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
增量更新(破坏第一个条件):(CMS用到了)
当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
原始快照(破坏第二个条件):(G1用到了)
当灰色对象要删除指向白色对象的引用关系时,就将这个要删 除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
即:将删除引用的白色对象作为根,重新扫描,保证当前白色对象不会被误删。不好的地方就是这个白色对象如果没有再被引用,也得等到下次垃圾收集时被回收(ps:这一块仅是自己的理解,后续还需要求证)
分代收集理论
因为将Java堆划分出不同的区域,所以才会有垃圾收集器每次只回收其中一个或某些部分的区域
为什么需要分代收集
如果一个区域内的大多数对象是朝生夕灭,难以熬过垃圾收集过程
那么将他们集中在一起,每次只需要考虑需要保留的少量存活对象而不是去标记那些大量需要回收的对象,
就可以以较低的成本回收大量的空间
同时,将那些难以回收的对象统一放在一个区域中,就可以以较低的频率去回收这块区域,
兼顾了垃圾收集的时间开销和内存空间的有效利用
相关名词
Partial GC(部分收集)才有了 Minor GC/Young GC(新生代收集)、Major GC/Old GC(老年代收集)、Full GC(整堆收集)、Mixed GC(收集整个新生代以及部分老年代,只有G1收集器有这样的行为)
也才能发展出 “标记-复制算法”,“标记-清除算法”,“标记-整理算法”
跨代引用问题
新生代的对象可能会被老年代引用,老年代的对象也有可能会被新生代引用
假如此时我们要回收新生代的对象,就需要去扫描整个老年代,这无疑是一个不合理的操作
此时会在新生代上建立一个全局的数据结构(记忆集 Remembered Set),将老年代划分成若干小块,标记处老年代的那一块内存存在跨代引用。然后在MinorGC的时候,只需要把包含了跨代引用的小块内存里面的对象假如到GC Roots进行扫描即可
关于记忆集,存在三种精度
- 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
- 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
- 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
目前我们用到的最多的一种就是卡表。卡表(Card Table)是卡精度的一种实现方式
卡表中的么一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作“卡页”(Card Page)
一个卡页的内存中通常不止一个对象,只要一个卡页内有一个(或多个)对象存在跨代指针,则就在对应卡表的数组元素的值标识位1,代表这个元素变脏。(GC时只需要筛选出卡表中变脏的元素,然后将其加入到GC Roots中一并扫描)
此处HotSpot使用写屏障来维护卡表。(可以使用类比的思想,AOP的Around来看待写屏障维护卡表的操作)在引用对象的赋值会产生一个环绕(Around)通知。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直 至G1收集器出现之前,其他收集器都只用到了写后屏障。
垃圾回收算法
- 标记-清除算法(标记过程也会有STW,但是标记一般很快)(HotSpot的CMS收集器,关注延迟)
- 标记-复制算法
- 标记-整理算法(移动必须要STW,对象越多越大耗时越长)(HotSpot的ParallelScavenge收集器,关注吞吐)
是否移动对象都存在弊端,移动则是在回收对象时复杂,不移动则是在内存分配时复杂。相比来说,内存分配和访问的频率会比回收高很多