深入理解Java虚拟机 第二章-垃圾回收

一、垃圾回收概述

1. 什么是垃圾?

垃圾是指在程序运行过程中,没有被任何指针引用的对象。

An object is considered garbage when it can no longer be reached from any pointer in the running program

2. 为什么需要GC?
  • 内存是固定的,只要程序不关闭,也不进行GC,那么内存中存储的obj或者说data会越来越多,内存迟早会被消耗完。
  • 除了释放没用的对象,垃圾回收也可以清除内存里的内存碎片。碎片整理将所占用的堆内存移到堆一端,以便JVM将内存分配给新对象。
  • 随着应用程序的业务量越来越大,没有GC就不能保证应用程序的正常运行。
3. 早期垃圾回收

在早期的C/C++时代,垃圾回收基本是手工进行的,开发人员用new申请内存,用delete关键字回收内存。这种方法虽然可以灵活控制对象的存活时间,但是加大了工作人员的任务量,同时,如果开发人员忘记回收内存,还有可能造成内存泄漏的事故。

4. Java垃圾回收机制

Java拥有自动的内存管理,可以降低内存溢出或者内存泄露的风险。同时可以使Java开发人员更加专注于业务开发。

二、垃圾回收相关概念

1. System.gc()

一般情况下,调用System.gc()或者Runtime.getRuntime.gc()会显式的触发FullGC,同时对堆区和方法区进行GC。同时,System.gc()方法带有一个免责声明,意思就是显式调用它,但是它并不一定会执行。而且垃圾回收是GC的自动行为,一般不建议手动调用,一般调用的情况只有我们正在编写一个性能测试。

2. 内存溢出和内存泄漏

内存溢出

所谓内存溢出就是没有空闲内存,并且GC也不能为内存提供内存,这时程序就会抛出OOM。

  • Java虚拟机的堆内存设置不够

可能是泄露问题,也可能是本身内存就过小,可以通过参数-Xms、-Xmx来调整。

  • 代码中创建了大量的大对象,并且长时间没有被GC(存在引用)
内存泄露(Memory Leak)

内存泄漏就是一个对象已经失去了作用,但是GC又没有办法区回收它。
在这里插入图片描述

上图正好说明了这个问题,当右边的对象引用被切断,按道理应该后面的对象会被回收,但是存在一个引用链没有被切断,这就导致那一堆对象都无法被回收(因为它们还存在这引用)。
举例

  1. 单例模式,因为单例模式的对象实例生命周期很长,如果一个对象和这个单例对象有引用关系,那么这个外部对象就无法被回收。
  2. 一些资源未被关闭,比如数据库连接,网络连接(socket)以及io。

3. Stop The World (STW)

Stop The World简称STW,指的是GC发生时,GC线程会将用户线程停顿,等待GC结束,再恢复应用程序。

4. 垃圾回收的并发与并行

并发

所谓并发并不是真正意义上的‘同时进行’,只是CPU将一个时间段划分为多个时间段,然后在这其中来回切换线程,因为CPU执行速度快,让用户感觉是多个应用程序在同时运行。
在这里插入图片描述

并行

当系统有一个以上的CPU,或者CPU有多个核心时,两个核心可以同时运行两个进程,进程间互不干扰,同时进行,我们将这称之为并行。
在这里插入图片描述

二者对比

并发
多个事情,在同一时间段同时发生。
并行
多个事情,在同一时间点同时发生。
并发的多个任务是互相抢占资源的,即在一个很小的时间段内,各个线程互相通过抢占CPU来获得执行的权力。并行的多个任务是不互相抢占资源的。

垃圾回收的并发与并行

并行
指的是多个GC线程在并行工作,用户线程处于等待状态。
并发
指用户程序与垃圾收集程序同时执行,垃圾回收线程在执行时不会停顿用户程序的运行。

  • 用户程序在继续运行,而垃圾收集线程运行在另一个CPU或线程上。
  • CMS、G1
    在这里插入图片描述

5. 安全点和安全区域

安全点

程序并非执行到任何位置都可以进行GC,只有在特定的位置才能开始GC,这些位置被称为安全点(Safepoint)
安全点的选取标准:是否具有让程序长时间执行的特征。比如选择一些指令执行时间较长的指令作为Safepoint,比如方法调用、循环跳转、异常调整等。

安全区域

安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域的任何位置进行GC都是安全的。

6. 引用(Reference)

强引用

最传统的‘引用’的定义,是指再程序代码中最常见的引用赋值。即类似于Object obj=new Object();强引用是GC永远不会收集的对象实例。但是,强引用也是内存泄漏的主要原因。

软引用

软引用就是用来描述一些有用,但非必须的对象,比如缓存。如果程序内存不足时,将要发生OOM时,GC才会把软引用的对象进行回收。

弱引用

弱引用的对象生命周期仅仅局限在下次GC发生前,一旦GC发生,弱引用的对象都会被回收。

虚引用

如果一个对象拥有虚引用,则它随时都有可能被GC回收,而且当试图通过虚引用来获取对象值时只能得到null。虚引用存在的意义只有追踪垃圾回收过程,因为虚引用的对象在被GC时,会收到一个系统通知。

引用小结

强引用永远不会被GC,它是代码中引用赋值最常见的场景。软引用和弱引用一般被用来作为缓存的存在,因为存在则可以提高系统速率,如果被回收也不会影响系统进程。虚引用一般用来对象的垃圾回收跟踪。

三、垃圾回收相关算法

在堆里几乎存放了所有的Java实例,在GC执行之前,首先需要分析出哪些对象是存活对象,哪些对象是死亡对象,只有被标记的死亡对象,才会被GC。 这个过程被称为标记阶段。下面将先介绍垃圾标记阶段相关算法:

1. 垃圾标记算法-引用计数算法

定义

引用计数算法的实现就是为每一个对象保存一个整型的计数,用来记录对象的被引用的情况。 对于一个对象A,如果有对象引用了它,则它的计数器就加1,引用失效就减1。

优点

操作简单,垃圾辨识度高,回收速度快。

缺点
  1. 因为为每个对象都维护一个计数器,则增加了内存空间的开销。
  2. 每次引用对象都需要更新计数器,则增加了时间开销。
  3. 如果遇到循环引用,则没有办法处理,有可能会因此造成内存泄漏。
    循环引用:objA->objB objB->objA objC->objA,如果objC不再引用A,则A,B不再存在作用理应被回收,但是因为他们各自的计数器都是1,又无法被回收。
    在这里插入图片描述
Python中使用了引用计数算法,它是如何解决循环依赖的呢?
  • 手动解除:在合适的时间,解除引用关系。
  • 使用弱引用weakref,weakref是Python提供的标准库,可以解决循环依赖。

2. 垃圾标记算法-可达性分析算法

定义

可达性分析算法就是以根节点集合为起始点,按照从上到下的方式搜索对象是否跟节点集合有可到达的线路。这个线路又被称为引用链。Java采用的标记算法就是可达性分析算法。
在这里插入图片描述

何为GC Roots?(根节点集合)-important
  • 虚拟机堆栈中引用的对象:

  • 本地方法栈在JNI(Java Native Interface)种引用的对象

  • 方法区中类静态属性引用的变量

  • 方法区中常量引用对象

字符串常量池中(StringTable)的引用

  • 所有被同步锁持有的对象
  • Java虚拟机内部引用

基本数据类型对应的类对象、一些常驻异常对象(如:NullPointerException、OutOfMemoryError)、系统类加载器。

  • 反映JMXBean内部的Java虚拟机、JVMTI寄存器回调、本地代码缓存等。

3. 对象的finalization机制

定义

JVM为对象提供了finalization机制。即在一个对象被GC前,它一定会先执行finalize()方法,同时开发人员可以重写finalize()方法来提供对对象销毁前的自定义处理。但这里不建议主动去调用对象的finalize()方法,应该交给垃圾回收机制调用,因为finalize()方法可能导致对象‘复活’,而且一个糟糕的finalize()方法会严重影响GC的性能。

对象的状态

由于finalization机制的存在,所以一个对象通常存在三种状态:

  • 可触及的:从GC Roots出发可以到达这个对象节点。
  • 可复活的:对象的引用不存在了,但是对象还未执行finalize()方法。
  • 不可触及的:对象的finalize()方法GC调用,并且没有被复活,则它就会进入不可触及的状态,不可触及的对象不可以被复活,因为对象的finalize()方法只能调用一次。对象只有在不可触及状态下,才能被GC回收。

4. 垃圾清除阶段算法 标记-清除(Mark-Sweep)算法

执行过程
  • 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象Header中记录为可达对象
  • 清除:Collector对堆内存从头到尾进行线性遍历,遍历每一个对象,将对象Header中没有标记记录的进行回收。
    PS:这里要注意的是不是标记垃圾,而是标记有用的对象,然后清除没有标记的对象。
    在这里插入图片描述
缺点
  • 效率一般
  • 在进行GC的时候,需要停止整个程序,用户体验差
  • 这种方式回收后的内存是不规整的,有内存碎片,需要额外维护一个空闲列表。
    这里可以联想到对象的创建过程中为对象分配内存:当内存规整时,采用指针碰撞的方式存放对象,即用指针记录当前内存的末尾,然后存放新对象后更新指针位置。当内存不规整时,维护一个空闲列表,用来记录内存中空闲的内存。
何为清除?

这里的清除不是真正意义上的吧垃圾内存置null,还是吧垃圾所在的内存存入空闲列表。

5. 垃圾清除阶段算法 复制(Copying)算法

执行过程

所谓复制算法,就是将内存划分为两半,进行GC的时候,先将存活的对象移动到内存的另一半,然后对于这一半剩下的所有对象(垃圾),直接回收这一半内存。
在这里插入图片描述

优点
  • 没有标记和清除阶段,速度快。
  • 复制存活的对象到另一半空间,内存规整,不存在碎片问题。
缺点
  • 很明显,浪费内存空间,因为它需要两倍内存。
  • 对于存活对象比较多的情况,它需要移动的存活对象多,这就代表了它需要维护的内存地址任务量就加重。
    PS:复制算法将存活对象移动到内存的另一半,内存地址改变了,所以需要对使用这个对象实例的引用也进行修改
应用场景

对于存活对象少,复制算法是很适合的。由此可以看出,在堆中,Yong区对象的存活时间很短,有80%左右的对象是朝生夕灭的,所以适合采用复制算法。事实也证明了如此,在Yong区确实采用了复制算法进行GC,特此解释了S0,S1的由来。

6. 垃圾清除阶段算法 标记-压缩(Mark-Compact)算法

执行过程

第一阶段和标记-清除算法一样,从GC Roots开始找到所有的垃圾并标记它们。然后将这些存活的对象实例依次规整的移动到内存的一段,按顺序排放。最后再回收内存的边界(垃圾所在的内存空间)。
在这里插入图片描述

对比

对比于标记-清除算法,标记-压缩算法就是标记-清除算法的升级版,它比标记-清除多做了一件事,就是将存活对象按顺序排放。本质上的差异就是一个是非移动式回收算法,一个是移动式回收算法。

优点
  • 消除了标记-清除算法中,内存不规整的缺点。
  • 消除了复制算法中,内存减半的代价。
缺点
  • 效率低
  • 移动对象的过程,还是会需要修改引用地址。
  • 移动过程,会触发STW

7. 分代收集算法(理论)

这里的算法并不是像上面说的那几种算法一样,它只是一种宏观上的操作。
所谓分代收集:就是将堆内存空间划分为年轻代和老年代。
年轻代:80%的对象朝生夕灭,对象生命周期短,存活率低,回收频繁。
老年代:对象存活时间长,回收次数少。
基于以上特点,所以采用分代收集的理论,年轻代采用复制算法,老年代采用标记-清除和标记-压缩结合使用。

8. 增量收集算法

所有的GC算法都难以避免STW的事件,如果STW事件持续时间过长,用户体验感就会很差,基于此,就有了增量收集算法。

基本思想

对于一整块区域的垃圾收集,每次收集,只让一个线程收集这个区域的一小块,然后恢复应用进程,下次再继续收集其他的小块区域,反复这个操作,直到GC完成。这样可以减缓一次STW的时间。

缺点

GC线程和应用程序线程的来回切换,势必会增加GC的成本上升,系统的吞吐量就会下降。

9. 分区算法

分代算法按照对象的生命周期长短划分为两个部分,而分区算法将堆内存空间划分为很多连续的不同小区间,每个小区间都独立使用。每次GC只收集其中一个或多个区域。在后续介绍的垃圾收集器中,G1采用的就是分区算法。
ps:增量收集算法是将总的收集量一部分一部分的去执行,而分区算法是将总的内存空间分为小分区,一次可控的去收集多少个小区间。

四、垃圾回收器的相关概念

1. 垃圾回收器的分类

按照线程数
  • 串行

串行垃圾回收器指的是单线程独占的GC,一般有Serial、Serial Old。

  • 并行

并行垃圾回收器指的是多CPU的情况下,各CPU同时执行垃圾回收。一般有Prallel Scavenge 、 Prallel Old 、PraNew。

按照工作方式
  • 并发式

并发式垃圾回收器允许用户线程和GC线程同时运行。

  • 独占式

独占式垃圾回收器当执行GC时,所有的用户线程全部停止,等GC结束,才能恢复。

按照碎片处理方式
  • 压缩式

压缩式GC会在GC完成后,对存活的对象实例进行压缩,消除回收后的碎片。

  • 非压缩式

非压缩式没有这部操作。

按照工作的内存区间
  • 年轻代GC
  • 老年代GC

2. 垃圾回收器的评测指标

吞吐量

运行用户代码占总运行时间的百分比

暂停时间

执行GC的时候,STW的时间

内存占用

Java堆区所占内存的大小。

说明

上述三项指标,我们需要重点关注暂停时间和吞吐量,因为随着时代发展,内存(硬件)方面的问题逐渐不再重要。高吞吐量会让应用程序的最终用户感觉只有用户线程在一直工作。而少的暂停时间,即低延迟可以给用户带来更好的交互体验。
如果选择吞吐量优先,则必须减少GC的次数,但这也意味着单次GC的时间会加长。
如果选择低延迟优先,则必须增加GC的次数,但这也会导致年轻代内存的缩减和程序吞吐量的下降。

标准

在最大吞吐量的情况下,降低GC停顿时间。

3. 不同垃圾回收器的概述

串行回收器

Serial、Serial Old

并行回收器

ParNew、Prallel Scavenge、Prallel Old

并发回收器

CMS(Concurrent Mark Sweep)、G1

垃圾回收器的组合使用与分类

在这里插入图片描述
在这里插入图片描述

如何查看默认的垃圾收集器
  • -XX:+PrintCommandLineFlags (查看命令行的相关参数,里面包括使用的垃圾收集器)
  • jinfo -flag 相关垃圾收集器参数 进程ID

五、七种垃圾回收器

1. Serial、Serial Old 介绍

Serial

Serial被运用在新生代进行GC,采用复制算法、串行回收以及STW机制进行回收。

Serial Old

Serial Old被运用在老年代进行GC,采用标记-压缩算法、串行回收以及STW机制进行回收。
Serial Old在Client模式下是默认的老年代垃圾回收器。在Server模式下主要和Parallel Scavenge配合使用。或者作为CMS的后备方案。

Serial 回收器在单CPU环境下,因为Serial没有和其他线程有交集,所以在专心做垃圾回收以获得最高的收集效率。运行在Client模式下,Serial是个不错的选择。
在HotSpot虚拟机中,可以使用-XX:+UseSerialGC来指定年轻代和老年代都使用串行回收器。(等价于年轻代用Serial,老年代用Serial Old)

2. ParNew回收器

ParNew是运行在年轻代的、采用复制算法、同样有STW的并行执行的垃圾回收器。
它和Serial 除了是并行工作的,没有任何区别。

参数设置
  • -XX:+UseParNewGC来指定年轻代使用该收集器。
  • -XX:ParallelGCThreads限制线程数量,默认情况下开启的是和cpu数据相同的线程数。

3. Parallel回收器

吞吐量优先

Parallel Scavenge回收器和ParNew一样,都是采用复制算法、并行回收、运行在年轻代和STW机制。但它出现的唯一目的就是新增了一项功能:自适应策略。它可以做到控制吞吐量的效果。Parallel Old则是用来取代Serial Old的回收器,运行在老年代,采用标记-压缩的算法,同样也是并行、拥有STW机制的。

场景

高吞吐量意味着可以高效率的运用CPU时间,适用于在后台运算而不需要太多交互的任务。例如:执行批量处理、订单处理、工资支付、 科学计算的应用程序。同时,Parallel Scavenge 和Parallel Old也是jdk1.8默认的垃圾收集器。
在这里插入图片描述

参数配置
  • -XX:UseParallelGC 手动激活Parallel运行在年轻代。
  • -XX:UseParallelOldGC 手动激活Parallel Old运行在老年代。其实这两个命令开启一个,另一个也会默认随之开启。
  • -XX:ParllelGCThreads设置并行回收开启的线程数,一般设置与CPU数量相等。
  • -XX:MaxGCPauseMillis设置垃圾收集器最大停顿时间(STW)
  • -XX:GCTimeRatio垃圾收集时间占总时间的比例
  • -XX:UseAdaptiveSizePolicy设置Parallel收集器具有自适应调节策略。

4. CMS(Concurrent-Mark-Sweep)回收器

CMS是工作在老年代,主要收集老年代的垃圾。CMS最大的特点就在于它极大的降低了GC的单次停顿时间,即拥有低延迟的特点。

场景

CMS因为低延迟的特性,则比较试用于一些互联网网站或者B/S架构的应用上,因为它的低延迟特性可以给用户带来较好的体验。

工作流程

在这里插入图片描述

初始标记

所谓初始标记,就是标记所有GC Roots能够直接关联的对象,这个阶段会停止所有用户线程,即会出现STW,但这个过程会很快,因为只需要关联roots直接关联的对象。

并发标记

这个阶段主要是并发执行的,GC线程和用户线程一起执行,并沿着刚开初始标记的对象继续往下标记GC Roots间接关联的全部对象。这个过程耗时很长但并不需要停顿用户线程。

重新标记

因为上一阶段的标记是和用户线程并发执行的,所以难免避免不了有些对象在并发标记时被标记为垃圾,但是因为用户线程也在运行,导致这些‘垃圾’可能又被其他对象引用。所以才有重新标记这个阶段,就是为了修正并发标记期间,因为用户线程继续运作而导致标记变动的那一部分对象的标记记录。

并发清理

此阶段就是让GC线程去清除标记阶段以及被判定死亡的对象。这里清理采用标记-清除算法。

优缺点
优点
  • 并发收集
  • 低延迟
缺点
  • 会产生内存碎片
  • CMS收集器对CPU资源很敏感,因为它是并发执行的,所以工作的时候会占用一部分CPU资源。
  • CMS收集器无法处理浮动垃圾。比如如果在并发标记阶段,又产生了新的垃圾对象,CMS是无法对这些垃圾进行回收的,进而导致新产生的这些垃圾对象无法被及时回收。
注意事项
  • CMS由于清除阶段是和用户线程并发执行的,所以只能使用标记-清除算法,而标记-压缩算法需要更改对象的地址,但用户线程还在执行,这显然是行不通的,所以造成了CMS清除垃圾后会造成内存碎片的问题。
  • 虽然CMS是并发执行GC的,但它的初始标记阶段和重新标记阶段还是会有很短暂的STW。
  • 因为最耗时间的并发标记和并发清理都是不需要暂停用户线程,所以整体的回收还是低延迟的。
  • 因为CMS并发执行GC过程中,用户线程并没有中断,所以需要确保用户线程有足够的内存使用,所以CMS不能等到老年代快满时才进行GC,它是有一个阈值的,当堆内存使用达到一定的阈值,CMS才开始工作,如果CMS运行时,用户线程内存还是不足,则会触发Serial Old收集器进行收集。
  • 因为CMS会产生内存碎片这一缺点,而且它不能和Parallel 收集器结合工作等原因,后续CMS也被jdk直接删除了。jdk9使用CMS会被警告CMS未来会被弃用,jdk14中则是直接删除了CMS收集器。
参数
  • -XX:UseConMarkSweepGC

手动指定CMS作用老年代的垃圾收集,该参数开启后,将自动绑定ParNew收集器也打开,因为它们是一起工作的

  • -XX:CMSInitiatingOccupanyFraction

设置堆内存使用率的阈值,即达到阈值就开始使用CMS进行GC。

  • -XX:UseCMSCompactAtFullCollection

用来指定当发生Full GC后对内存空间进行压缩,以此避免内存碎片的问题。但是因为压缩是不能和用户线程并发执行的,所带来的问题就是停顿时间变长了。

  • -XX:CMSFullGCsBeforeCompaction

设置进行多少次Full GC后对内存空间进行压缩压缩。

  • -XX:ParallelCMSThreads

设置CMS并发执行时的线程数量。

5. G1(Garbage First)回收器

G1是可以作用于年轻代和老年代的一款“全功能收集器”。官方给G1设定的目标是在延迟可控的情况下尽可能多的获得大的吞吐量。在jdk7中被引用,jdk9中被设置为默认垃圾收集器。

为什么叫G1(Garbage First)

因为G1的收集是基于分区实现的,它将堆内存划分为很多大小相同的区域(Region),使用不同的Region来表示Eden、S0、S1、Old。然后G1有计划的避免一次性对整个Java堆进行回收。而是通过跟踪每个Region的价值(GC所能回收的空间以及GC的消耗时间),在后台维护一个优先列表,==每次根据指定的收集时间,首先回收价值最大的Region或者多个Region。==所以G1的侧重点在于Region能回收垃圾的最大量,所以取名‘垃圾优先’(Garbage First)。

G1的优势
  • G1在多核且内存大的环境下,可以极高概率的满足GC停顿时间的需要,又能兼顾高的吞吐量。
  • 并发与并行
  • 并行:在多核CPU的情况下,G1可以有多个GC线程同时执行,有效的利用了多核的特性,此时用户线程处于STW。
  • 并发:G1还拥有与用户线程同时执行的能力,因为不会出现GC时完全停止用户线程的现象。
  • 分代收集
  • 从分代上看,G1依然属于分代型垃圾收集器,它会区分年轻代和老年代,以及年轻代中的Eden区和Survivor区。但从堆结构来看,它不要求整个年轻代、老年代在逻辑上是连续的。因为年轻代和老年代都被划分为了很多个Region。
  • 对比其他收集器,它兼顾了年轻代和老年代。因为年轻代和老年代的GC它都可以。

在这里插入图片描述

  • 空间整合

CMS是标记-清除会产生内存碎片,但是G1的GC是基于Region的,不管是Region之间采用的复制算法,还是整体上来看的标记-压缩算法,它们都不会产生内存碎片。

  • 可预测的时间停顿模型

G1除了追求低停顿时间外,它还能建立可预测的时间停顿模型。即它可以让用户明确指定一个长度为M毫秒的时间片段,尽可能的将GC的收集时间控制在不超过M毫秒。这也是划分众多Region的原因。因为G1的收集会根据M毫秒的设置,去列表中选择一个或多个可以回收的Region(这些可回收的Region相加回收时间不超过M毫秒)。保证G1可以在有限时间内获取尽可能高的收集效率。

参数设置
  • -XX:UseG1GC

手动指定垃圾回收器采用G1,jdk9中才被设为默认,jdk8及以前要使用都需要手动指定。

  • -XX:G1HeapRegionSize

设置G1划分的每个Region大小。值是2的幂,范围是1MB-32MB。

  • -XX:MaxGCPauseMillis

最大停顿时间,即G1尽可能去控制GC的控制时间不超过此设置。

区域(Region)

在G1中,所有的Region大小相同,且在JVM生命周期内不会被改变。虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。
在这里插入图片描述
上图解释:

  • Region只能是Eden、Survivor、Humongous中的一种,但是它的身份不是固定的,谁来占用那么这个Region就是谁的。
  • 图中的E表示该region属于Eden内存区域,s表示属于survivor内存区域,o表示属于Old内存区域。图中空白的表示未使用的内存空间。
  • G1垃圾收集器还增加了一种新的内存区域,叫做Humongous 内存区域,如图中的H。主要用于存储大对象,如果超过1.5个region,就放到H。
  • Humongous区是用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。
G1的回收过程

在这里插入图片描述

总体概述
  1. 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程。 G1的年轻代收集阶段是并行的独占式收集器。 在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到survivor区间或者老年区间,也有可能是两个区间都会涉及。
  2. 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。这个过程年轻代的GC也可能也在工作。
  3. 标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。G1回收器对老年代的回收和其他GC不同,G1每次回收只需要扫描并回收小部分老年代的Region就可以了。 同时,这个老年代Region是和年轻代一起被回收的。
  4. 如果需要,单线程、独占式的Full GC还是继续存在的,它是针对GC失败时的一种失败保护机制,即强力回收。
记忆集(Remembered Set)

每个Region内都可能存在被其他Region引用的对象。那如果每次GC都需要扫描整个堆内存需要耗费很多时间,会降低GC效率。
Remembered Set 就是记录当前Region中哪些对象是被外部Region所引用的。
解决方法:

  • 设置Remembered Set避免JVM的全局扫描。
  • 每次Reference类型数据写操作时,都会产生一个写屏障(Write Barrier)暂时中断操作。
  • 然后检查将要写入的对象是否和它的引用数据在不同的Region (其他收集器:检查老年代对象是否引用了新生代对象)
  • 如果不同,通过CardTable把相关引用记录到该对象的所在Region对应的Remembered Set中
  • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏。
    在这里插入图片描述
回收过程详解
  1. 年轻代GC
  • 第一阶段,扫描根。
    可以体现Rset作用:避免全堆扫描。
    根引用(GC Roots)连同RSet记录的外部引用一起作为扫描存活对象的入口。
  • 第二阶段,更新RSet。
    作用:保证Rset中的数据准确性。
    处理(脏卡表)Dirty Card Queue中的card,更新RSet。此阶段完成后, RSet可以准确的反映其他Region对当前Region中对象的引用。
  • 第三阶段,处理RSet。
    作用:根可达性遍历的一部分。
    识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
  • 第四阶段,复制对象。
    说明:新生代使用复制算法
    此阶段,对象树被遍历, Eden区内的Region中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内Region中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的Region。如果Survivor空间不够, Eden空间的部分数据会直接晋升到老年代Region。
  • 第五阶段,处理引用。
    空Eden: Eden变成空的,那它就变成了无主Region,因此会被记录到空链表中,等待下一次被分配。
    处理Soft, weak, Phantom, Final, JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

什么是脏卡表(Dirty Card Queue)?
对于应用程序的引用赋值语句object.field=object , JM会在之前和之后执行特殊的操作以在Dirty Card Queue中入队一个保存了对象引用信息的card,在年轻代回收的时候, G1会对Dirty Card Queue中所有的card进行处理,以更RSet,保证RSet实时准确的反映引用关系。

脏卡表队列作用:Reset更新需要线程同步,所以开销会很大,因此不能实时更新,因此我们需要把引用对象被其他对象引用的关系放在一个脏卡表队列中,当年轻代回收的时候会进行STW,所以我们也正好把脏卡表队列中的值更新到Rset中,这样不仅没有涉及到开销问题,还可以保证Rset中的数据是准确的。

  1. 并发标记过程
  • 初始标记阶段
    标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
  • 根区域扫描(Root Region Scanning)
    主要扫描哪些老年代对象是可达的毕竟我们进行young GC的时候会移动Survivor区,移动之后就找不到哪些老年代对象是可达的了
  • 并发标记(Concurrent Marking)
    在整个堆中进行并发标记(和用户程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
  • 再次标记(Remark)
    由于应用程序持续进行,需要修正上一次的标记结果,同时也是是STW的。
  • 独占清理(cleanup,STW)
    计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域,为下阶段做铺垫,同时也是是STW的。其实是一个统计计算过程,不会涉及垃圾清理。
  • 并发清理阶段
    识别并清理完全空闲的区域。
  1. 混合回收
  • 并发标记结束以后,老年代中全都都是垃圾的Region被回收了,部分为垃圾的Region被计算了出来。默认情况下,老年代的Region的GC会被分为8次。
  • 混合回收的回收集(Collection Set)包括八分之一的老年代内存分段, Eden区内存分段, Survivor区内存分段。混合回收算法和年轻代回收算法一致,只是多了老年代的Region。
  • 由于老年代中的Region默认分8次回收, G1会优先回收垃圾多的Region。垃圾占Region比例越高的,越会被先回收。
  • 混合回收并不一定要进行8次。有一个阈值默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。
  1. 可能会发生的Full GC
  • 如果G1的回收失败,则会启用单线程,STW的Full GC。

6. 7种垃圾收集器总结

在这里插入图片描述

六、小结

  • 没有一种GC收集器是完成没有STW的。
  • 随着硬件与科技的发展,GC收集器会做的越来越好。
  • 没有最好的收集器,更没有万能的收集。
  • 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值