GC垃圾回收

Awe 6月前 ⋅ 74 阅读

一、判断垃圾回收的两种方式

1.引用计数法:给对象中添加一个引用计数器,每当有一个地方引动该对象的时候,引用计数器+1,当引用失效的时候,计数器就-1。计数器为0的时候对象就是没有被使用的。 优点:实现方式简单,高效。 缺点:很难解决对象之间相互循环引用的问题。(即堆中有对象A和对象B,两个对象之间相互引用,两个对象的引用计数器都不会为0,GC垃圾回收器不会回收) 2.可达性分析法:通过"GC Roots"的对象作为起点,从这些起点向下搜索,节点所走过的路成为引用链,没有任何引用链连接的对象,则是没有用的对象,GC垃圾回收器回收这些对象。

二、什么是GC Roots?

1.虚拟机栈中的引用对象(栈中的本地变量表,即局部变量表) 2.本地方法栈中的JNI(一般是Native方法)引用的对象 3.方法区中静态属性引用的对象 4.方法区中常量引用的对象

三、STW分析

全称Stop The World,在GC事件发生的过程中,会产生应用程序的卡顿。停顿产生时整个应用程序线程都会停止。 例如:可达性分析算法中枚举根节点(GC Roots)会导致所有的java线程停顿。

1.停顿原因

分析工作必须在一个确保一致性的快照中进行。 一致性指整个分析期间整个执行系统被冻结在某个时间点上。 如果分析过程中对象的引用关系还在不断地变化,则分析结果的准确性无法保证。

2.STW分析

被STW中断的应用程序线程在完成GC之后恢复,保证用户体验需要减少STW的发生。 STW事件和采用哪款GC无关,所有的GC都有这个事件 G1也不能避免STW的发生,只是尽可能的缩短了暂停的时间。 STW是JVM后台自动发起和完成的。在用户不可见的情况下,把用户在正常的工作线程全部停掉 开发中采用System.gc(),会导致Stop The World的发生。

四、判断废弃常量

常量池存在字符串“abc”,如果当前没有任何String对象引用该字符串常量的话,说明该字符串就是废弃常量。 注意:JDK1.7之后将运行时常量池从方法区中移除,在堆中开辟区域存放运行时常量池。

五、判断无用的类

需要同时满足三个条件 1.该类所有的实例都被回收 2.加载该类ClassLoader被回收 3.该类对应的java.lang.Class对象在任何地方都没有被引用,无法在任何地方通过反射访问该类的方法 注意:满足三个条件仅仅只是可以被回收,不是像对象一样不使用了必然被回收。

六、垃圾收集的算法

1.标记-清除算法

标记阶段:标记出所有需要回收的对象 清除阶段:标记完成后统一回收所有被标记的对象 两个明显的问题:

  • 效率问题:效率较低
  • 空间问题:标记清除后产生大量不连续碎片

2.复制算法

为了解决效率问题,“复制”算法出现了。将内存分为大小相同的两块,每次使用其中的一块。当这一块内存使用完之后,将这一块内存中还存活的对象复制到另一块内存中去,再把使用的空间一次性清掉。每次回收的内存都是对内存区间的一半进行回收。

3.标记-整理算法

根据老年代特点提出的一种标记算法,标记过程仍与"标记-清除"算法一样。然后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存

4.分代收集算法

根据对象存活周期的不同将内存分为几块。一般java堆分为新生代和老年代。可以根据各个年代的特点选择合适的垃圾回收算法。

新生代:每次收集都有大量的对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次的垃圾收集。 老年代:老年代的存活几率较高,没有额外的内存对它进行分配担保,必须选择“标记-清除”或者“标记-整理”算法进行垃圾回收

七、HotSpot为什么要分为新生代和老年代

1.为什么会有年轻代

分代的唯一理由就是优化GC性能。如果没有分代,所有对象在一起,GC的时候寻找无用的对象会对堆中的所有区域进行扫描。很多对象都是朝生夕死,分代的话,将新创建的对象放到某个区域,GC的时候先把这块存有“朝生夕死”的对象的区域进行回收,腾出很大空间来。

2.年轻代中的GC

年轻代有三个部分Eden区,和两个Survivor区(分别叫做from和to)。默认比例8(Eden):1(Survivor)。一般情况下会将新创建的对象分配到Eden区(一些大对象进行特殊处理,分配到老年代),这些对象经过一个minor GC之后,年龄会增加1岁,当年龄达到一定程度时(一般默认15岁),就会被移入老年代。因为年轻代中“朝生夕死”的对象很多,所以年轻代使用的是复制算法。当GC开始的时候,对象只会存在Eden和from的Survivor区,Survivor的to区是空的。进行GC的时候,将Eden区中的对象会被直接复制到to区中去。而在From区中的对象会根据他们的年龄值来决定去向。年龄达到阈值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到老年区,没有达到阈值的对象会被复制到“to区”。经过这次GC后,Eden区和from区已经被清空。然后from和to区进行角色互换。不管怎样都会保证to区是空的。Minor GC会一直重复这样的过程,直到TO区被填满,to区填满之后会将所有对象移动到老年代。

3.为什么要有Survivor区

没有Survivor区,Eden区每执行一次 Minor GC存活的对象都会被送到老年代。老年代很快被填满,触发Major GC(Major GC一般伴随着Minor GC,可以看做一个Full GC)。老年代的空间大于新生代,Full GC的消耗的时间比Minor GC 的长的多。 没有Survivor区解决方法: 1.增大老年代的空间: 优点:更多存活对象才能填满老年代,减低FulL GC的频率。 缺点:随着老年代空间增大,一旦发生Full GC,执行所需要的时间更长 2.减少老年代的空间: 优点:Full GC所需要的时间减少 缺点:老年代很快被存活对象填满,Full GC 的频率增加。 结论:Survivor存在的意义,就是减少送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经理过16次Minor GC 的还能在新生代存活的对象,才会被送到老年代

4.为什么要设置两个Survivor区

为了保证整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。保证复制算法的运行以提高效率。

八、常见的垃圾回收器有哪些

  • Serial收集器:串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿。新生代,老年代串行回收;新生代复制算法,老年代标记-压缩算法。垃圾收集的过程中会产生STW。
  • ParNew收集器:是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法,老年代标记-压缩算法。
  • Parallel收集器:Paraller Scavenge收集器类似于ParNew收集器,Paraller收集器更关注于吞吐量。
  • Paraller Old收集器:是Paraller Scavenge收集器的老年代版本,使用多线程和标记-整理算法
  • CMS收集器:获取最短停顿时间为目标的收集器。CMS是基于“标记-清除”算法实现的。
  • G1收集器:采用标记-整理算法,不会产生内存空间碎片。可停顿预测。

九、简单介绍CMS和G1收集器

1、介绍CMS收集器

CMS收集器是一种以获取最短停顿时间为目标的收集器,非常注重用户体验上的应用。是一款并发收集器,实现了垃圾线程于用户线程基本上同时工作。

- CMS收集器的四个步骤

1.初始标记:暂停其他所有线程,并直接记录下与root相连的对象。 2.并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。GC和用户线程同时开启,用户线程可能会不断地更新引用域,GC线程无法保证可达性分析的实时性。这个算法会跟踪记录这些引用发生更新的地方。 3.重新标记:修正并发标记期间因用户程序继续运行而导致标记变动的那一部分对象的标记记录。 4.并发清除:开启用户线程,同时GC线程对标记区域进行清扫。

- CMS的优缺点

优点:并发收集,低停顿。 缺点:对cpu资源敏感。无法处理浮动垃圾。会产生大量内存空间碎片。

2、介绍G1垃圾收集器

G1是一款面向服务器的垃圾收集器,主要针对多颗处理器及大容量内存的区域,以极高概率满足GC停顿时间要求的同时,还满足高吞吐量。

- G1的特点
  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU缩短STW的时间
  • 分代收集:G1不需要其他收集器配合就可以独立管理整个堆,但是保留了分代概念
  • 空间整合:G1整体来看是标记-整理算法;局部基于复制算法实现
  • 可预测停顿: G1能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内。
- G1收集器的步骤
  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

** G1收集器在后台维护了一个优先列表,每次根据允许收集时间,优先选择回收价值最大的Region。**

十、Minor GC与Full GC有什么不同

  • 新生代GC(Minor GC):发生在新生代的垃圾回收动作,java对象大多都是朝生夕死的,所以Minor GC非常频繁,一般回收速度较快。
  • 老年代GC(Major GC /Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次Minor GC(不绝对,在Paraller Scavenge收集器的策略中就有直接Major GC的策略选择过程)。Major GC一般要比Minor GC慢十倍以上。
  • 从年轻代回收成为Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来讲的。

十一、Full GC触发条件

  • System.gc()的调用
  • 老年代空间不足
  • 永生代空间不足:方法区中需要加载的类,反射的类,调用的方法较多时,方法区可能会被占满,在未配置为CMS GC的情况下也会产生Full GC。
  • CMS GC出现promotion failed和concurrent mode failure:promotion failed是在Minor GC中,Survivor区放不下,对象只能放入老年代,而老年代空间也放不下造成的。concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。(有时候空间不足时CMS GC的浮动垃圾过多导致的暂时性空间不足而触发Full GC)
  • 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的晋升空间。 在进行Minor GC的过程中做一个判断,如果之前统计的Minor GC 晋升到旧生代的平均大小大于旧生代的剩余空间直接触发Full GC。
  • 堆中分配很大的对象。所谓大对象,是指需要大量连续内存空间的java对象。例如很长的数组,这种对象会直接进入老年代,老年代虽然有很大的空间,但是无法找到足够大的连续空间来分配给当前对象,这种情况也会触发Full GC。

十二、oopMap

1、根节点枚举

  • 所有收集器在根节点枚举的时候都是需要暂停用户线程的,枚举的过程必须在一个保证“一致性”的快照中进行
  • GC Roots的对象引用就那么几个,主要是全局性的引用(常量或者静态变量)与执行上下文(虚拟机栈帧中引用的对象),虽然目标明确,但是查找的过程中做到快速高效并不是一件很容易的事情。
  • 将引用对象和他对应位置的信息用哈希表记录下来,GC的时候直接读取这个哈希表,用于存储引用类型的数据结构就叫OopMap

2、安全点Safe Point

  • 特定的位置生成OopMap,记录对象引用的相关信息。

  • 强制要求程序必须执行的到安全点才能够进行GC。

  • 安全点的设定既不能太少以至于让垃圾收集器等待时间过长,也不能太多以至于进行频繁的垃圾收集从而导致运行时的内存负荷增大

  • 安全点的选定是以** 是否具有让程序长时间执行的特征为标准选定的。典型的是指令序列的复用**:方法的调用,循环跳转,异常跳转等等

    ** 如何让GC发生时让所有用户线程都执行到最近的安全点,然后停顿下来?** 1.抢先式中断:GC发生时,系统将所有的用户线程都中断,如果发现有用户线程中断的位置不在安全点上,就恢复这条线程执行,直到跑到安全点上在重新中断。

  • 优点:思路简单

  • 缺点:时间成本不可控,导致性能不稳定和吞吐量的波动 2.主动式中断:不会直接中断线程,全局设置一个标志位,用户线程不断轮询这个标志位,标志位为真时,线程会在最近的一个安全点主动中断挂起。

3、安全区域

  • 安全点保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集的过程的安全点。
  • 不活跃的线程没有获得CPU时间吗,没法轮询标志位,也没法找到最近的安全点主动中断挂起。
  • 安全区域:在一段代码片段中,引用关系不会发生变化,因此在这个区域中任意地方开始GC都是安全的。
  • 当用户线程执行到安全区域中的代码时,会标志自己进入安全区域。当虚拟机要发起GC时,不必去管安全区域内的线程。当安全区域中的线程被唤醒并离开安全区域时,需要检查主动式中断策略的标志位是否为真(虚拟机是否处于STW状态),如果为真则继续挂起等待,如果为假则标志还没有开始STW或者STW刚刚结束,线程就可以被唤醒然后继续执行。

十三、跨代问题

1.Minor GC的时候,从GC Roots出发那不也会扫描到老年代的对象吗?那不就相当于全局扫描?

JVM的解决办法:在老GC中(G1以下)是要求整个GC堆是在一个连续的地址空间上的,所以会有一条分界线。

2.年轻代的对象被老年代引用?

  • HotSpot虚拟机下是由card table(卡表)来避免全局扫描老年代对象
  • 堆内存的每一小块形成卡页,卡表就是卡页的集合。当判断卡页中有对象存在跨代引用,将这个页标记为脏页。
  • 每次进行Minor GC的时候只需要区卡表中找到脏页,找到后加入GC Root,不用遍历整个老年代对象。

十四、三色标记法

1.三色标记法把遍历对象图解的过程中遇到的对象,按照是否访问过这个条件标记成以下三种颜色:

  • 白色:表示对象尚未被垃圾收集器访问过
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已被扫描过
  • 灰色:表示对象已经被垃圾收集器访问过,且这个对象至少存在一个引用还没有被扫描过

2.标记过程分析

  • 步骤一:GC并发标记刚开始时,所有对象均为白色
  • 步骤二:将所有GCRoots直接引用的对象标记为灰色集合
  • 步骤三:判断灰色集合中的对象: 1.若对象不存在子引用,则将其放入集合 2.若对象存在子引用,则将其所有的子引用对象放入灰色集合,当前对象放入黑色集合
  • 步骤四: 按照步骤三以此类推,直到灰色集合中的对象变为黑色后,本轮标记完成。且当前在白色集合内的对象称为不可达对象,既垃圾对象。

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

1.浮动垃圾
  • 垃圾收集与用户线程是并行的,但这个对象实际已经死亡了,没有引用,被垃圾收集器错误的标记为存活对象
2.对象消失(漏标)
  • 把原本存活的对象错误的标记为已消亡
  • 对象消失的两个条件: 1.插入一条或者多条从黑色对象到白色对象的引用 2.删除了全部从灰色对象到白色对象的直接或者间接引用
3.遍历对象图不需要STW的解决方案

1.增量更新:破坏第一个条件,当黑色对象插入新的指向白色对象的引用时,将这个新的插入记录下来,等并发扫描结束后,再将这些记录过的引用关系中的黑色对象为根,重现扫描一次。简单理解为:黑色对象一旦插入指向白色对象的引用关系后,他就变为灰色对象 2.原始快照:破坏第二个条件,当灰色对象要删除指向白色对象的引用时,要将这个删除的引用记录下来,并发扫描结束后,再将记录过的引用关系中的灰色对象,重新扫描。可以理解为:无论删除与否,都按照刚刚开始扫描的那一刻的对象图快照来扫描。 3.CMS基于增量更新做并发标记,G1和Shenandoah则是用原始快照来实现的

十五、CMS垃圾回收器

  • 如果使用seria和parallel系列的收集器:在垃圾回收时,用户线程都会完全停止,直至垃圾回收结束。
  • CMS垃圾回收器与上面的垃圾回收器(seria和paralle)最大的不同就是并发,在GC线程工作的时候,用户线程不会完全停止,用户线程在部分场景下与GC线程一起并发执行。
  • CMS的设计目标是避免老年代GC出现长时间的卡顿。

1、CMS的工作流程

CMS可以简单分为初始标记,并发标记,(并发预清理),重新标记,以及并发清除。

  • 初始标记过程:标记与GC Root直接关联的对象以及年轻代指向老年代的对象,发生STW不向下追溯
  • 并发标记过程:不会STW,GC Roots向下追溯,标记所有可达对象(对于GC来说,比较耗时),并发标记用户线程没有被挂起,对象是有可能发生变化的
  • 并发预处理:希望减少下一个阶段重新标记所消耗的时间。老年代引用发生变化(标记dirty),重新标记处理;新生代有可能有新的引用指向老年代,重新标记处理(这个过程可能发生Minor GC来减少扫描时间)
  • 重新标记过程:会产生STW,停顿时间很大程度上取决于并发预处理阶段(一边标记存活对象,一边用户线程在执行产生垃圾)
  • 并发清除过程:不会STW,一边用户线程在执行,一边GC线程在回收不可达对象,用户线程可能不断产生垃圾,叫浮动垃圾,留到下一次GC处理

2、CMS缺点

  • 空间需要预留:CMS可以一边处理用户线程,一边进行GC垃圾回收,需要有充足的内存空间供用户使用,如果CMS预留空间不够,会报错(Concurrent mode Failure),会启动Serial Old垃圾收集器进行老年代回收,导致时间较长的STW
  • 浮动垃圾:垃圾回收和用户线程是同时进行的,在标记或者清除的时候,用户的线程也会改变对象的引用,原本某些对象不是垃圾,在CMS垃圾清理的时候变成了垃圾,CMS无法回收,只能等到下一次GC。CMS垃圾收器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”导致另一次Full GC的产生
  • 内存碎片:CMS本质上是标记清除算法,会产生内存碎片,碎片过多导致内存不足产生Full GC。在Full GC 中CMS会对内存碎片进行整理。整理涉及到标记整理算法,会STW

十六、G1垃圾回收器

  • CMS垃圾回收期的停顿时间不可预知
  • G1垃圾收集器可以设定一个希望的STW时间,G1垃圾回收器会根据这个时间尽量满足
  • JVM堆中的内存分布以物理空间隔离,在G1中堆的划分是以逻辑的形式进行划分
  • G1中还存在分代概念,堆被划分出了多个同等区域,在G1中每个区域叫做Region
  • G1中有新生代,老年代,Survivor,大对象区,一旦发现没有引用指向大对象,就可直接在年轻代中的Minor GC中直接回收
  • 堆内存大的时候,每次垃圾回收需要对一整块很大的内存区域进行回收,时间不好控制,划分多个小区域之后,对这些小区域回收容易控制收集时间

G1的GC过程

  • G1收集器,可以分为Minor GC和Mixed GC,有些特殊场景会用到Full GC
1、Minor GC
  • G1的Minor GC触发时机与前面的垃圾收集器一样,Eden区满之后触发Minor GC,会发生STW。
1.Minor GC的回收过程
  • 简单分为三步:根扫描,更新&&处理RSet,复制对象
  • 根扫描:可以理解为CMS中初始标记的过程
  • 处理&&更新RSet:CMS中利用卡表避免全表扫描老年代的对象,G1中使用RSet,RSet这种储存在每个Region都会有,记录其他Region引用了当前Region的对象关系,年轻代Region中的RSet只保存老年代的引用,老年代的Region也只会保存老年代对它的引用。所以第二部,处理RSet的信息并扫描,将老年代持有年轻代对象的引用加入到GC Roots,避免被回收掉。
  • 将扫描之后的存活的对象往空的Survivor或者老年代存放,其他的Eden区清除
  • 在Minor GC的最后,会处理软引用,弱引用,JNI Weak等引用,结束收集
2、Mixed的过程
  • 当堆的占用率达到一定阈值之后会触发MIixed GC(默认45%,参数决定)
  • 全局并发标记与CMS过程非常相似,步骤是:初始标记(STW)、并发标记、最终标记(STW)、清理(STW)
  • 一定Mixed GC一定会回收年轻代,采集部分老年代Region进行回收,是一个混合GC
  • 初始标记:共用了Minor GC 的STW(Mixed GC一定会发生Minor GC),复用了扫描GC Roots的操作,新生代老年代都会扫。
  • 并发标记:不会STW,GC线程与用户线程一起执行,GC线程收集各个Region的存活对象的信息,向下追溯,查找整个堆存活的对象
  • 重新标记:与CMS一样,标记那些并发标记阶段发生变化的对象
1.CMS在重新标记阶段,应该会扫描所有的线程栈和整个年轻代的作为Root,G1好像不是这样
  • G1采用STAB算法,在GC开始时,为存活的对象做了一次快照
  • 并发阶段,把每一次发生过引用变化的旧值给记录下来,重新标记阶段只扫描发生过变化的引用,看看对象是否存活,加入到GC Roots
  • STAB的问题:在开始时,G1认为它是活的,此次GC中不会对他回收,即便在并发阶段已经变为垃圾。所以G1也会存在浮动垃圾。总的来说,对G1来说问题不大,不追求一次把垃圾都清除掉,注重STW时间。
  • 清理阶段:会进行STW,,主要清点和重置标记状态。根据停顿预测模型,决定本次GC回收多少Region
  • Mixed GC会选定所有年轻代Region,部分回收价值高的老年代Region(回收价值高就是垃圾多)进行采集,最后Mixed GC进行清除通过拷贝的方式进行。
  • 一次回收未必是将所有垃圾回收,G1会根据停顿时间做出选择的Region的数量

3、G1会什么时候发生Full GC

  • 如果在Mixed GC中无法跟上用户线程分配内存的速度,导致老年代填满无法继续进行Mixed GC,又会降级到Serial Old GC来收集整个GC Heap
  • 这个场景相对于CMS较少,G1中没有CMS中内存碎片的问题

全部评论: 0

    我有话说: