JVM垃圾回收
1.概述
JVM(Java虚拟机)的垃圾回收是自动管理内存的过程,用于回收不再使用的对象并释放内存资源。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。这种自动化的内存管理机制极大地减轻了开发人员对手动释放内存的负担,提高了开发效率。
1.1.什么是垃圾
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。
1.2.早期的垃圾回收
在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。比如以下代码:
MibBridge *pBridge= new cmBaseGroupBridge();
//如果注册失败,使用Delete释放该对象所占内存区域
if (pBridge->Register(kDestroy) != NO ERROR)
delete pBridge;
这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。
1.3.Java的垃圾回收
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险。没有垃圾回收器,java也会和c++一样,各种悬垂指针,野指针,泄露问题让你头疼不已。自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。
对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见outofMemoryError时,快速地根据错误异常日志定位问题和解决问题。
垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收。其中,Java堆是垃圾收集器的工作重点
Oracle关于垃圾的介绍
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html
2.对象存活判断
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
2.1引用计数法
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点
实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性
缺点
-
它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
-
每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
-
引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
循环引用
当p的指针断开的时候,内部的引用形成一个循环,这就是循环引用
小结:
引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。
Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试
2.2可达性分析法
Java、C#等主流语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的
-
可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
-
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
-
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
-
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
蓝色:存活对象 白色:可回收对象
GC Roots
在Java语言中,GC Roots包括以下几类元素
- 虚拟机栈中引用的对象,比如:各个线程被调用的方法中使用到的参数、局部变量等
- 本地方法栈内JNI(通常说的本地方法)引用的对象
- 方法区中类静态属性引用的对象 ,比如:Java类的引用类型静态变量
- 方法区中常量引用的对象,比如:字符串常量池(String Table)里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(PartialGC)
如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GCRoots集合中去考虑,才能保证可达性分析的准确性。
3.垃圾回收算法
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在JVM中比较常见的三种垃圾收集算法是标记一清除算法(Mark-Sweep)、复制算法(copying)、标记-压缩算法(Mark-Compact)
3.1.标记-清除算法(mark-sweep)
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
-
标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
-
清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
-
标记无引用的死亡对象所占据的空闲内存,并记录到空闲列表中(free list)。
-
当需要创建新对象时,内存管理模块会从 free list 中寻找空闲内存,分配给新建的对象。
-
这种清理方式其实非常简单高效,但是也有一个问题内存碎片化太严重了。
-
Java 虚拟机的堆中对象,必须是连续分布的,所以极端的情况下可能即使总剩余内存充足,但寻找连续内存分配效率低,或者严重到无法分配内存
缺点
-
标记清除算法的效率不算高
-
在进行GC的时候,需要停止整个应用程序,用户体验较差
-
这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表
3.2.标记-复制算法(mark-copy)
为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。
核心思想
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
- 图上看这回做完垃圾清理后连续的内存空间就大了。
- 它的好处很明显,就是解决内存碎片化问题。但也带来了其他问题,堆空间浪费了一半
优点
-
没有标记和清除过程,实现简单,运行高效
-
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点
-
此算法的缺点也是很明显的,就是需要两倍的内存空间。
-
对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
-
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行
应用场景
在新生代,对常规应用的垃圾回收,一次通常可以回收70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
3.3.标记-压缩算法(mark-compact)
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生。
1970年前后,G.L.Steele、C.J.Chene和D.s.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。
执行过程
1.第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
2.第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
3.之后,清理边界外所有的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
优点
-
消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
-
消除了复制算法当中,内存减半的高额代价。
缺点
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序。即:STW
3.4.对比
Mark-Sweep | Mark-Compact | Copying |
---|---|---|
中等 | 最慢 | 最快 |
少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍空间(不堆积碎片) |
否 | 是 | 是 |
为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。没有最好的算法,只有最合适的算法。
3.5.分代收集
前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。目前几乎所有的GC都采用分代手机算法执行垃圾回收的。
年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
-
Mark阶段的开销与存活对象的数量成正比。
-
Sweep阶段的开销与所管理区域的大小成正相关。
-
Compact阶段的开销与存活对象的数据成正比。
以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。
分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代
4.并行与并发
4.1.并发(Concurrent)
在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。
4.2.并行(Parallel)
当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel)。其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。适合科学计算,后台处理等弱交互场景
4.3.并发 vs 并行
-
并发,指的是多个事情,在同一时间段内同时发生了。
-
并行,指的是多个事情,在同一时间点上同时发生了。
-
并发的多个任务之间是互相抢占资源的。
-
并行的多个任务之间是不互相抢占资源的。
-
只有在多CPU或者一个CPU多核的情况中,才会发生并行。
-
否则,看似同时发生的事情,其实都是并发执行的。
4.4.垃圾回收的并发与并行
4.4.1并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。如ParNew、Parallel Scavenge、Parallel Old;
4.4.2.串行(Serial)
相较于并行的概念,单线程执行。如果内存不够,则程序暂停,启动JM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。
4.4.3并发(Concurrent)
5.垃圾回收器
有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection,对应的产品我们称为Garbage Collector。
-
1999年随JDK1.3.1一起来的是串行方式的serialGc,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本
-
2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2一起发布·
-
Parallel GC在JDK6之后成为HotSpot默认GC。
-
2012年,在JDK1.7u4版本中,G1可用。
-
2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。
-
2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
-
2018年9月,JDK11发布。引入Epsilon 垃圾回收器,又被称为 "No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)
-
2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。·
-
2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。
-
2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macos和Windows上的应用
-
串行回收器:Serial、Serial Old,并行回收器:ParNew、Parallel Scavenge、Parallel old,并发回收器:CMS、G1
官方手册:https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf
5.1Serial回收器 串行回收
-
Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
-
Serial收集器作为HotSpot中client模式下的默认新生代垃圾收集器。
-
Serial收集器采用复制算法、串行回收和"stop-the-World"机制的方式执行内存回收。
-
除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial Old收集器同样也采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。
-
Serial old是运行在Client模式下默认的老年代的垃圾回收器
-
Serial 0ld在Server模式下主要有两个用途:① 与新生代的Parallel scavenge配合使用 ② 作为老年代CMS收集器的后备垃圾收集方案
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)
优势:
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。运行在Client模式下的虚拟机是个不错的选择。
5.2ParNew回收器 并行回收
ParNew收集器则是Serial收集器的多线程版本。Par是Parallel的缩写,New:只能处理的是新生代。ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制
-
对于新生代,回收次数频繁,使用并行方式高效。
-
对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)
由于ParNew收集器是基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比serial收集器更高效?
-
ParNew 收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。
-
但是在单个CPU的环境下,ParNew收集器不比Serial 收集器更高效。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。
5.3.Parallel回收器 并行回收
HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制。
-
和ParNew收集器不同,ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器
-
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序
-
Parallel 收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器
-
Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-the-World"机制
-
这个也是JDK8默认使用的垃圾回收器
5.4.CMS回收器 并行回收
在JDK1.5时期,Hotspot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
-
CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
-
目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
-
CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the-World"
-
不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
-
在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。
-
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的
优点
-
并发收集
-
低延迟
弊端
-
会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发FullGC。
-
CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
-
CMS收集器无法处理浮动垃圾。可能出现“
Concurrent Mode Failure
"失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact?
答案其实很简单,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact更适合“Stop the World” 这种场景下使用
5.5.G1回收器 区域化分代式
G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等
G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征
在JDK1.7版本正式启用,移除了Experimenta1的标识,是JDK9以后的默认垃圾回收器,取代了CMS回收器以及Parallel+Parallel Old组合。被Oracle官方称为“全功能的垃圾收集器”,与此同时,CMS已经在JDK9中被标记为废弃(deprecated)
5.5.1.G1回收过程一:年轻代GC
-
JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。
-
年轻代垃圾回收只会回收Eden区和Survivor区。
-
首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。
然后开始如下回收过程:
-
第一阶段,扫描根。根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口。
-
第二阶段,更新RSet。处理dirty card queue(见备注)中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用。
-
第三阶段,处理RSet。识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
-
第四阶段,复制对象。此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
-
第五阶段,处理引用。处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
5.5.2.G1回收过程二:并发标记过程
-
初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
-
根区域扫描(Root Region Scanning):G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在YoungGC之前完成。
-
并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被YoungGC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
-
再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning(SATB)。
-
独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。这个阶段并不会实际上去做垃圾的收集
-
并发清理阶段:识别并清理完全空闲的区域
5.5.3.G1回收过程三:混合回收
当越来越多的对象晋升到老年代o1d region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。
5.6.七款经典收集器与垃圾分代之间的关系
-
新生代收集器:Serial、ParNew、Parallel Scavenge;
-
老年代收集器:Serial Old、Parallel Old、CMS;
-
整堆收集器:G1;
5.7.垃圾回收器组合
两个收集器间有连线,表明它们可以搭配使用:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
- 其中Serial Old作为CMS出现"
Concurrent Mode Failure
"失败的后备预案。 (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial old这两个组合声明为Deprecated(JEP 173),并在JDK 9中
完全取消了这些组合的支持(JEP214),即:移除。
-
(绿色虚线)JDK 14中:弃用ParallelScavenge和SeriaOold GC组合(JEP 366)
-
(绿色虚框)JDK 14中:删除CMS垃圾回收器(JEP 363)
5.8.垃圾回收器总结
截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器
垃圾收集器 | 分类 | 作用位置 | 使用算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行运行 | 作用于新生代 | 复制算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
ParNew | 并行运行 | 作用于新生代 | 复制算法 | 响应速度优先 | 多CPU环境Server模式下与CMS配合使用 |
Parallel | 并行运行 | 作用于新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
Serial Old | 串行运行 | 作用于老年代 | 标记-压缩算法 | 响应速度优先 | 适用于单CPU环境下的Client模式 |
Parallel Old | 并行运行 | 作用于老年代 | 标记-压缩算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
CMS | 并发运行 | 作用于老年代 | 标记-清除算法 | 响应速度优先 | 适用于互联网或B/S业务 |
G1 | 并发、并行运行 | 作用于新生代、老年代 | 标记-压缩算法、复制算法 | 响应速度优先 | 面向服务端应用 |