目录
4.7 CMS (Concurrent-Mark-Sweep)
1. STW(Stop the World)
通常指的是JVM在垃圾回收中,执行垃圾回收算法时,会暂停所有应用线程,所有代码停止。这是因为在标记阶段,使用可达性分析算法进行分析时,整个应用程序的数据都应该处于“一致性视图”当中,这是为了保证可达性分析算法的准确性,因此需要暂停所有的用户线程,假如不停止,gc线程标记了当前需要被回收得对象,这对象在别的线程突然又被使用,会造成错误得回收结果。
2. MinorGC/FullGC
MinorGC:对堆中新生带(Eden区)的垃圾回收,对于大多数应用minorGC的STW时间可以忽略不计
FullGC:对整个堆空间、方法区的回收,耗费的时间将会多得多,JVM优化很大一部分考虑是,如何让系统运行过程中几乎不发生FullGC。
还有其他种类,比如Major GC只回收老年代(CMS收集器中支持)。
3. 垃圾收集算法
垃圾回收两个阶段,首先需要判断哪些对象存活(垃圾标记),接着是如何对死亡对象清理(垃圾清理)。
3.1 垃圾标记
判断哪些对象存活,即怎么才算是死亡的对象,简单的说就是对象没有任何地方可以被引用到,即算死亡。一般由两种方式,引用计数和可达性分析。
3.1.1 引用计数
为每个对象把保存一个整型的引用计数器,来记录该对象被引用的情况,有任何一个对象引用了该对象,计数器加1,为0则表示该对象可以被回收。实现简单,判断方便。但是需要额外字段来记录并且每次复制都需要更新计数器,增加了空间和时间开销,而且无法解决循环引用问题,例如 A a 和B b两个对象,A对象中引用B对象, B对象中引用A对象,然后A a = null ,B b = null; 此时a b都是垃圾,但是引用计数算法无法处理这种情况,导致内存泄漏。
3.1.2 可达性分析
又称根搜索或追踪性垃圾收集,可以有效解决循环引用问题。可达性分析算法是以根对象集合(GC Roots,堆中存活对象都直接或间接被根对象集合连接)为起始点,从上到下搜索可访问的对象,搜索的路径称为引用链,如果对象没有任何引用链相连,则意味着对象可以回收,为垃圾对象。
这里GCRoots的定义极为重要,因为要想实现完整且正确的追踪垃圾收集,就必须完整列举所有GCRoots,否则就会漏扫导致错误回收了活对象。枚举根节点(GC Roots)会导致所有Java执行线程停顿,以保证分析工作在一个能确保一致性的快照中进行。
GC Roots主要包括以下几类元素:
- 虚拟机栈中引用的对象、方法局部变量等
- 本地方法栈引用的对象
- 方法区中静态属性引用的对象
- 方法区常量引用的对象
对于堆分代的情况来说,分为新生代和老年代,分开垃圾回收,例如YoungGC,新生代属于收集部分,老年代属于非收集部分,因为必然存在老年代指向新生代的对象引用,所以这也必须作为GCRoots的一部分。
3.1.3 Object.finalize()
不建议使用,没必要。
finalization机制允许开发人员在垃圾回收对象销毁之前,添加自定义处理逻辑,垃圾回收该对象之前,总会先调用该对象的finalize方法。
finalize()方法执行是在GC之前,若此对象不发生GC,将不会被调用。
finalize()方法可能会导致对象复活,被调用时当然是因为此对象在外界没有任何可以被引用到的地方,如果在方法中将本对象赋值给外界引用,那么该对象会“复活”,不再被垃圾收集器看作垃圾对象。一个对象的finalize只会调用一次。
也就是说,判定一个对象真正属于垃圾对象,有两阶段,如果没有引用链,进行第一次标记,接着判断是否需要执行finalize(),如果没有重写finalize()或者已经执行过一次finalize(),那么这个对象没救了,准备回收之。如果重写了且还没执行过,则会将该对象插入个队列随后执行其finalize()方法,方法中若与引用链上的对象建立了联系,那么该对象在第二次标记时,不再被标记为垃圾对象。
3.2 垃圾清除
3.2.1 标记-清除
使用可达性分析算法标记出存活对象,收集器再遍历堆内存,如果发现对象没有被标记,则将其回收也就是清除。
缺点:此算法清理出的空间是不连续的,产生了内存碎片,因此需要维护一个内存空闲列表,每次new对象时,检查空闲列表找到一个满足大小的空间分配,再更新空闲列表。
3.2.2 复制
将内存空间一分为二,每次只是用其中一块,垃圾回收时每次将存活对象复制到另一块内存,再将原来那块全部清除。 保证了空间的连续性,不存在碎片。缺点也很明显,有一半的内存区被浪费。
对于新生代来说,绝大多数对象都是朝生夕死,垃圾对象很多,复制算法需要复制的存活对象数量不会太大,因此经过研究和优化,没有必要将内存按1比1分配,而是将内存分为较大的eden和两个较小的suvivor区,每次复制将eden和一块survivor复制到另一块survivor,默认大小比例为8:1:1,这样仅有10%的空间被浪费,此时复制算法的效率是很高的。但我们无法保证单块survivor每次回收一定能放下存活的对象,因此需要老年代来分配担保,survivor放不下了,直接进入老年代
3.2.3 标记-整理
对于老年代,大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。又因为标记清除会造成内存碎片,所以改进之后产生标记整理算法。
第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
之后,清理边界外所有的空间。
执行效果相当于标记-清理后,再执行一遍内存整理,效率上来说较低。内存整理以后,对象按内存地址一次排列,这样在new对象分配内存时,不需要再维护空闲列表,而是另一种分配方式,称为指针碰撞。
指针碰撞
内存中已使用和未使用各自一边,只需记录一个中间的分割点地址指针,再分配新对象内存时,只需将指针向后移动该对象的大小。
标记压缩算法优点
- 内存有序
- 给新对象分配内存时,JVM只需要持有一个内存的起始地址
- 解决复制算法内存减半问题
标记压缩算法缺点
- 标记整理算法效率上要低于复制算法
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。移动过程中,需要ST
3.2.4 分代
堆中因对象生命周期不同分为年轻代和老年代,针对不同代采用不同的垃圾回收机制,即就是分代收集算法。
年轻代因为内存大小相对较小,生命周期短,回收频繁,存活率低使用复制算法效率最高。内存浪费也由Eden和两个Survivor区优化,利用率默认90%。
老年代内存大,对象生命周期长,存活率高,回收不频繁,一般由标记清除或标记整理混合实现。
3.2.5 增量
上述算法,在垃圾回收过程中,应用程序处于STW状态,挂起等待GC完成,时间过长影响用户体验。
增量的思想就是一次性收集会导致长时间停顿,那能否每次只收集一小部分区域,接着切换会应用程序,以此减少应用STW时间,这样反复折腾直到全部完成。通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是传统的标记一清除和复制算法。
4. 垃圾收集器
垃圾回收的串行、并行和并发
串行,应用STW,单线程执行垃圾回收
并行,应用STW,多线程执行垃圾回收
并发,应用线程和垃圾回收线程同时执行,交替工作减少STW停顿时间
根据收集区域分类
- 新生代收集器: Serial、 ParNeW、Parallel Scavenge
- 老年代收集器: Serial Old、 Parallel Old、 CMS
- 整堆收集器: G1
GC收集器的关注点 吞吐量和停顿时间
吞吐量:程序的运行时间/程序的运行时间+内存回收的时间,例如总运行时间一个小时,垃圾回收总时间一分钟,吞吐量就是59/60。吞吐量优先需要降低GC频率,但每次GC的停顿时间也更长。
停顿时间:执行垃圾收集时,STW的时间,停顿时间有限即减少每次STW的时间,但因此也会导致更频繁的回收,吞吐量下降。
停顿时间越短越适合与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率利用CPU时间,尽快完成程序运算任务,适合在后台运算而不需要太多交互的任务
各种收集器各有优劣,根据应用场景需要选择合适的收集器来提高性能。
4.1 查看当前应用使用的垃圾收集器
各种GC相关参数说明
- -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC
- -XX:+UseParNewGC:标明新生代使用ParNew GC,同时老年代使用Serial Old GC
- -XX:+UseParallelGC:表明新生代使用Parallel Scavenge
- -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC,ParallelGC和ParallelOldGC二者相互激活
- -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用,ParNew+CMS+Serial Old的收集器组合
- -XX:+UseG1GC:使用G1收集器
1、-XX:+PrintCommandLineFlags: 查看命令行相关参数(包含使用的垃圾收集器)
启动打印:
-XX:InitialHeapSize=264765056 -XX:MaxHeapSize=4236240896 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
2、jinfo -flag 。。。。 pid
4.2 Serial
Client模式下的默认新生代垃圾收集器。采用复制算法,单线程回收,回收时STW
4.3 Serial Old
Client模式下默认的老年代的垃圾回收器。 采用标记压缩算法,单线程回收,回收时STW。
Serial Old目前主要使用于1)新生代的ParallelScavenge配合使用 2)作为老年代CMS收集器的备用处理。
Serial系列目前在一般交互web应用中远远无法满足需求,只有单核CPU中会被使用。
4.4 ParNew
Serial收集器的多线程版本,Parallel New,并行收集新生代。能与老年代CMS收集器配合工作。
-XX:ParallelGCThreads设置线程数量
4.5 Parallel Scavenge
与ParNew一样也是多线程,复制算法,收集新生代,收集时STW,不过他的关注点在吞吐量(Throughput),不能和CMS配合使用
提供两个参数来控制最大GC停顿时间(-XX:MaxGCPauseMills)和吞吐量大小(-XX:GCTimeRatio)
MaxGCPauseMills 设置一个大于0的毫秒数,收集器尽可能保证暂停时间不超过设置的值,但是缩短GC时间是以牺牲吞吐量来换取
GCTimeRatio设置一个大于0小于100的整数,例如设置为49,那允许的最大GC时间栈总时间的1/(1+49) ,允许最大2%的垃圾回收时间
自适应调节:-XX:+UseAdaptiveSizePolicy,设置以后虚拟机会根据当前系统运行情况来动态调整新生代区域大小比例等参数来提供最大的吞吐量
4.6 Parallel Old
老年代的并行收集器,标记压缩算法,收集时STW。 JDK8默认是Parallel 收集器和Parallel Old收集器的组合
4.7 CMS (Concurrent-Mark-Sweep)
HotSpot的并发老年代收集器,采用标记一清除算法,垃圾收集线程与用户线程同时工作,但是也存在STW,CMS的关注点在尽可能缩短垃圾收集时用户线程的停顿时间。适合和用户交互,有着良好的响应速度,用户体验较好。
4.7.1 CMS执行过程
整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。
初始标记:STW,然后标记出GCRoots能直接关联到的对象,速度很快
并发标记:从GC Roots的 直接关联对象开始遍历整个对象图的过程,耗时较长,但是和用户线程并发运行
重新标记:STW,在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录.
并发清除:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间,与用户线程同时并发的。
4.7.2 相关分析
- 并发标记与并发清除阶段都不需要STW,所以整体的回收是低停顿的,其他两个阶段有STW,但是时间较短。
- 因为清理过程和用户线程并发,所以还需要确保在应用运行过程中依然有足够的内存可用。因此不能在老年代满了以后才开始回收,而是当内存使用率达到一定阈值后,便开始回收。如果预留的内存仍然不能满足需要,将会临时启动Serial Old来进行老年代收集,这样回收时间、STW时间就大大延长,因此阈值要设置合适。
- 标记清除会产生内存碎片,因此无法使用指针碰撞分配内存,只能使用空闲列表。
- 为什么不内存整理,因为如果内存整理,势必会需要用户线程暂停(STW),无法并发回收。
- 那么内存碎片问题怎么解决?内存碎片太多,导致大对象无法分配的时候会进行一次整理。详看下节CMS参数
4.7.3 CMS相关参数
- -XX:+UseConcMarkSweepGC 开启CMS回收老年代,ParNew回收新生代
- -XX:CMSInitiatingOccupancyFraction,触发CMS回收的老年代空间使用率阈值,例如设置80,即表示老年代使用率80%以上,才会启动CMS收集,默认是-1,-1表示不使用此参数
- CMSInitiatingOccupancyFraction默认不使用,而使用下边两个参数来计算启动CMS的老年代使用率阈值:((100 - MinHeapFreeRatio) + (double)(CMSTriggerRatio * MinHeapFreeRatio) / 100.0) / 100.0,默认是92%
- 补充:以上参数为设置垃圾回收的阈值,但不是绝对,例如老年代剩余空间不足以满足新生代的晋升,这时即使没有到达阈值,也会频繁进行old gc。我遇到的情况:老年代较小512M,新手代1536M,survivor200M,老年代剩余的空间大小小于survivor已占用大小时,会频繁进行old gc
4. -XX:+UseCMSCompactAtFullCollection,执行完FullGC后是否对内存空间进行压缩,默认是可开启的
FullGC指的是内存碎片多导致大对象无法分配时 或者是 GC过程中程序继续运行产生的浮动垃圾导致内存被填满时触发(Serial Old) ,和CMS本身的并发收集不一样,会进行内存整理,停顿时间会变长。
5. -XX:CMSFullGCsBeforeCompaction设置在执行多少次Full GC后对内存空间进行压缩整理,默是0,即每次如果CMS不能支持内存分配了,就会压缩整理。
4.8 G1
简单记录,后续跟进
- G1 即Garbage First , 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region) (物理上 不连续的)。使用不同的Region来表示Eden、S0区,S1区,老年代等,优先回收垃圾最多的Region。
- 回收时以分区Region为单位,将存活的对象复制到另一个空闲分区, 没有物理上新生代和老年代的区别,只有逻辑上的概念,即一个region可能在不同代间来回切换。
- 面向服务端,针对具有大内存、多处理器、需要低GC延迟的应用。
- -XX:+UseG1GC 启用G1回收
- -XX:G1HeapRegionSize=n 通过参数-XX:G1HeapRegionSize=n指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
- -XX:MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标。 默认值是200ms
RememberedSet
一个Region中可能存在被其他Region引用的对象,那么在回收本Region标记垃圾对象时,GCRoots怎么定义,需要将全堆扫描吗?不需要,采用的就是Rset(其他收集器也是),每个区都有一个Rset记录本区对象被另外区对象引用的关联关系,在每个引用类型写操作时,判断是否指向的对象是在本区域,如果不是记录Rset。当垃圾回收时,把本区域的Rset加入GCRoots。