前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GC算法和垃圾收集器

GC算法和垃圾收集器

作者头像
leobhao
发布2022-06-28 18:30:18
3720
发布2022-06-28 18:30:18
举报
文章被收录于专栏:涓流

判断对象是否存活

垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”(即不可能再被任何途径使用的对象)、

引用计数算法(Reference Counting)

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。

引用计数算法的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存,其中最主要的一个原因是它很难解决对象之间相互循环引用的问题。

例如:在testGC() 方法中,对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外这两个对象再无任何引用,实际上这两个对象都已经不能再被访问,但是它们因为相互引用着对象方,异常它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

代码语言:javascript
复制
public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
     * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
     */
    private byte[] bigSize = new byte[2 * _1MB];
    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        //假设在这行发生了GC,objA和ojbB是否被回收
        System.gc();
    }
}
可达性分析算法(GC Roots Analysis)

主流的判断算法,这个算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会标记为是可回收对象。

在Java语言里,可作为GC Roots对象的包括如下几种:

  1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  2. 方法区中的类静态属性引用的对象(static 属性)
  3. 方法区中的常量引用的对象
  4. 本地方法栈中JNI的引用的对象
finalize()方法最终判定对象是否存活

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。

真正的判断一个对象死亡,至少要经过俩次标记过程:如果对象在进行根搜索后发现没有与GC roots相关联的引用链,那他将会第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这俩种情况都视为“没有必要执行”。

两次标记过程:

1)第一次标记并进行一次筛选。

筛选的条件是此对象是否有必要执行finalize()方法。

当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

2) 第二次标记

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为 F-Queue 的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F -Queue 队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。

Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己—-只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

整个流程如下:

GC回收动作

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式:
    • Young GC:只收集young gen的GC, 也叫Minor GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
Yonug GC

Young GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。

当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Young GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ) ,这些对象就会成为老年代。

Young GC触发条件: 一般是新生代中Eden区满时,Young GC

Full GC

Full GC 就是收集整个堆,包括新生代,老年代,永久代(在JDK 1.8及以后,永久代会被移除,换为metaspace)等收集所有部分的模式

针对不同的垃圾收集器,Full GC的触发条件可能不都一样

最简单的分代式GC策略,按HotSpot VM的serial GC(serial+serial old)的实现来看,触发条件是

  • young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
  • full GC:当准备要触发一次youngGC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc() 、heap dump带GC,默认也是触发full GC。

GC算法

标记-清除算法 (Mark-Sweep)

标记-清除算法将垃圾回收分为两个阶段:

  1. 标记阶段:首先标记出所有需要回收的对象
  2. 清除阶段:标记完成后,统一回收被标记的对象

缺点:

  1. 效率问题:标记清除过程效率都不高
  2. 空间问题:标记清除之后会产生大量的不连续的内存碎片(空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续的内存空间而不得不提前触发另一次垃圾收集动作)
复制算法 (Copying)

复制算法过程:

  1. 将现有的内存空间分为两快,每次只使用其中一块.
  2. 当其中一块时候完的时候,就将还存活的对象复制到另外一块上去
  3. 再把已使用过的内存空间一次清理掉

优点:

  1. 由于是每次都对整个半区进行内存回收,内存分配时不必考虑内存碎片问题。
  2. 只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效

缺点:

  1. 内存减少为原来的一半,太浪费了
  2. 对象存活率较高的时候就要执行较多的复制操作,效率变低

jvm新生代对此算法进行了修改,并不需要根据1:1划分内存空间,而是将内存划分为一块较大的EdenSpace和两块较小的SurvivorSpace,如图:

标记-整理算法 (Mark-Compact)

由于复制算法的缺点,及老年代的特点(存活率高,没有额外内存对其进行空间担保),老年代一般不使用复制算法。

标记-清除过程:

  1. 标记阶段:首先标记出所有需要回收的对象。
  2. 让存活的对象向内存的一端移动。而不跟“mark-sweep”直接对可回收对象进行清理
  3. 再清理掉边界以外的内存。

过程如图:

由于老年代存活率高,没有额外内存对老年代进行空间担保,那么老年代只能采用标记-清理算法或者标记整理算法。

分代收集算法 (Generational Collecting)

把Java堆分为新生代和老年代。根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,选用:复制算法

在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-压缩”算法来进行回收。

垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。

gc收集器的各个年代分布,?代表g1收集器, 连线代表可以搭配使用:

Serial收集器

默认的新生代收集器

特点
  1. 单线程的收集器,说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作
  2. 在它进行垃圾收集时,必须暂停其他所有的工作线程(Sun将这件事情称之为“Stop The World”),直到它收集结束。这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是难以接受的。

采用收集算法:复制算法(copying)

搭配:CMS 或Serial Old(MSC)

优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。

缺点: GC时暂停线程带给用户不良体验

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为都与Serial收集器完全一样。同样适用于新生代

ParNew是除了Serial之外唯一能与CMS配合工作的,但是由于多线程的交互开销,在单CPU的情况下,效果并不比serial好

特点
  1. 多线程GC(并行):ParNew是Serial的多线程版本,两者共用了许多代码
  2. 在GC时暂停所有用户线程

采用收集算法:复制算法(copying)

搭配:CMS 或Serial Old(MSC)

优点:高效

缺点:GC时暂停线程带给用户不良体验,单线程下效果不一定优于Serial

Parallel Scavenge收集器

使用于新生代, Parallel Scavenge主要在于精确的控制吞吐量, 适合后台运算、交互不多的任务(CMS等则是尽可能的缩短垃圾收集时用户线程停顿的时间, 用于交互任务)

特点
  1. 多线程GC(并行)
  2. 在GC时暂停所有用户线程

与其他收集器不同的是:

1)ParNew,CMS等收集器的关注点在于尽可能缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。[备注:吞吐量:运行代码时间/(运行用户代码时间+垃圾收集时间)]

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

2)Parallel Scavenge可采用GC自适应的调节策略。 使用自适应的调节策略: 即不需要指定新生代的大小,Eden与Surivior的比例,晋升老年代的年龄等细节参数,虚拟机自动根据根据当前系统的状态动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量。

参数:用于精确控制吞吐量

  • -XX:MaxGCPauseMillis 最大垃圾收集停顿时间
  • -XX:GCTimeRatio 垃圾收集时间与运行用户代码时间的比例=垃圾收集时间/运行用户代码时间,相当于是吞吐量的倒数。
  • -XX:+UseAdaptiveSizePolicy GC自适应的调节策略。

采用收集算法:复制算法(copying)

搭配:Parallel Old或Serial Old(MSC)

Serial Old(MSC)收集器

适用 1).运行在Client模式下的虚拟机中的老年代 2).在Server模式下,它主要还有两大用途 ①.与Parallel Scavenge搭配 ②.作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用

特点: 1.单线程GC,Serial收集器的老年代版本 2.在GC时暂停所有用户线程

算法:采用标记-整理算法

优点:简单,高效

缺点:GC时暂停线程带给用户不良体验

搭配:Serial Old(MSC)或ParNew

Parallel Old收集器

Parallel Scavenge的老年代版本,适用于注重吞吐量和CPU资源敏感的场合

特点 1).多线程GC(并行):Parallel Scavenge的老年代版本 2).在GC时暂停所有用户线程

算法:采用标记-整理算法

优点:高效

搭配:Parallel Scavenge(这个组合适用于一些长期运行且对吞吐量要求较高的后台程序)

CMS(Concurrent Mark Sweep)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

CMS GC 的设计目标是避免在老年代垃圾收集时出现长时间的卡顿,主要通过两种手段来达成此目标:

  1. 不对老年代进行整理,而是使用空闲列表(free-lists)来管理内存空间的回收。
  2. 在 mark-sweep(标记—清除)阶段的大部分工作和应用线程一起并发执行。

特点: 牺牲了吞吐量,但是响应速度很快。

工作过程
  1. 初始标记:这个阶段会 STW,这个阶段是为了标记存活的对象,有两部分: (1) 老年代所有的 GC Roots (2) 年轻代存活的引用老年代的对象

此阶段的优化,减少停顿时间,启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数,线程数不要超过cpu的核数。

  1. 并发标记:从“初始标记”阶段标记的对象开始找出所有存活的对象(即沿着GC Roots往下走)。这个阶段是并行的,不会产生STW(实际上所有的并行过程都不会STW) 。但是由于是并发进行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等。该阶段会把上述对象所在的Card标识为Dirty
  2. 预清理阶段: 这个阶段就扫描上个阶段所有的dirty card,处理前一个阶段因为引用关系改变导致没有标记到的存活对象.
  3. 可终止的预处理:这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生abort的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止(此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻带的引用,是的下个阶段的重新标记阶段,扫描年轻带指向老年代的引用的时间减少)
  4. 重新标记(Remark):这个阶段会导致第二次STW。该阶段的任务是完成标记整个年老代的所有的存活对象,但是会扫描整个堆,包括年轻代和老年代(因为老年代的对象被年轻代引用也会视为存活对象)

此阶段的优化: 这个阶段要处理年轻代和老年代的引用关系,会消耗大量的时间STW,可以在remark之前先执行一次ygc,并将对象放入幸存代或晋升到老年代,这样扫描的时间就会大大减少.参数:-XX:+CMSScavengeBeforeRemark

  1. 并发清理: 通过以上5个阶段,老年代所有的存活对象已经被标记,这个时候就会执行清理操作。由于是并行执行的,运行期间自然会有新的垃圾产生,只能下一次GC清理,这部分垃圾称为浮动垃圾
  2. 并发重置,进行一些重置工作以便进入下一次 CMS GC
CMS 使用总结

缺点: 1). 对CPU资源非常敏感。主要是并发标记阶段会与用户线程交替运行,会抢占一部分CPU资源

2).无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。 浮动垃圾:在并发清除阶段,用户线程仍在运行,这段时间用户线程产生的新的垃圾, 这部分CMS无法在当次收集。

也是因为并发清除阶段用户线程还需要运行,所以无法等老年代几乎填满了才运行, 需要预留一部分空间给用户线程在清除阶段使用, 可以通过-XX:CMSInitiatingOccupancyFration 来设置触发百分比, 如果CMS期间预留内存不足,则会出现concurrent mode fai lure, 此时虚拟机启动预备方案,临时启用Serial Old进行回收(full gc)。

3).产生空间碎片,影响大对象的分配。 这是由于该收集器是由“标记-清楚”算法实现的所引起的。所以往往存在有很大空间剩余,当无法找到足够大的连续空间来分配当前对象,不得不提前出发一次Full GC。

解决:

  1. -XX:+UseCMSCompactFullCollection 开关参数(默认开启)用于当CMS要进行Full GC时开启内存碎片的合并整理过程,该过程不能并发,故停顿时间变长。
  2. -XX:CMSFullGCsBeforeCompaction 用于设置执行多少次不压缩的Full GC后跟着来一次带压缩的Full GC。默认为0,表示每次进入Full GC时都进行碎片整理。

4). 减少 Remark 阶段 STW 的时间,-XX:+CMSScavengeBeforeRemark remark 之前先进行一次 ygc

优化总结
  1. 一般CMS的GC耗时80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数: -XX:+CMSScavengeBeforeRemark。在执行remark操作之前先做一次Young GC,目的在于减少年轻代对老年代的无效引用,降低remark时的开销。

2. 内存碎片问题。CMS是基于标记-清除算法的,CMS只会删除无用对象,不会对内存做压缩,会造成内存碎片,这时候我们需要用到这个参数:-XX:CMSFullGCsBeforeCompaction=n。 意思是说在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。

  1. promotion failed(提升失败)。在 Minor GC 过程中,Survivor 可能不足以容纳 Eden 和另一个 Survivor中的存活对象,那么多余的将被移到老年代,称为过早提升(Premature Promotion) ,这会导致老年代中短期存活对象的增长,可能会引发严重的性能问题。再进一步,如果老年代满了,Minor GC 后会进行 Full GC,这将导致遍历整个堆,称为提升失败(Promotion Failure)。
  2. concurrent mode failure异常。执行CMS GC的过程中,同时业务线程也在运行,当年轻带空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS 还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。如果发生这个异常就会导致CMS退化成Full GC.这里看起来和 promotion failed很相似,实际上是不同时期发生的事情

设置触发CMS GC触发的两个参数:

  • -XX:CMSInitiatingOccupancyFraction=70,在老年代内存使用达到70会触发gc
  • -XX:+UseCMSInitiatingOccupancyOnly, 如果不设置这个参数,上面的阈值只会第一次GC有效,后续会自动上调导致上面的参数不生效

默认情况下是 ParNew(年轻代)+CMS(老年代)

G1(Garbage First)收集器

G1 的主要关注点在于达到可控的停顿时间,在这个基础上尽可能提高吞吐量。G1 被设计用来长期取代 CMS 收集器,和 CMS 相同的地方在于,它们都属于并发收集器,在大部分的收集阶段都不需要挂起应用程序。区别在于,G1 没有 CMS 的碎片化问题(或者说不那么严重),同时提供了更加可控的停顿时间。

如果你的应用使用了较大的堆(如 6GB 及以上)而且还要求有较低的垃圾收集停顿时间(如 0.5 秒),那么 G1 是你绝佳的选择,是时候放弃 CMS 了。

G1概述
  1. G1 的角色 G1 将整个堆划分为一个个大小相等的小块(每一块称为一个 region),每一块的内存是连续的。和分代算法一样,G1 中每个块也会充当 Eden、Survivor、Old 三种角色,但是它们不是固定的,会随着GC 的过程转换角色(其实还有一种区块叫 humongous, 专门用来存放大对象的)。
  1. 优先收集垃圾多的分区

执行垃圾收集时,和 CMS 一样,G1 收集线程在标记阶段和应用程序线程并发执行(也会伴随着STW),标记结束后,G1 也就知道哪些区块基本上是垃圾(存活对象极少),G1会先从这些区块下手,因为从这些区块能很快释放得到很大的可用空间(这也是G1名字的由来,优先收集垃圾多的分区).

  1. 可控的停顿预测

在 G1 中,目标停顿时间非常非常重要,用 -XX:MaxGCPauseMillis=200 指定期望的停顿时间。G1 不是一个实时收集器,它会尽力满足我们的停顿时间要求,但也不是绝对的,它基于之前垃圾收集的数据统计,估计出在用户指定的停顿时间内能收集多少个区块。G1 使用了停顿预测模型来满足用户指定的停顿时间目标,并基于目标来选择进行垃圾回收的区块数量。G1 采用增量回收的方式,每次回收一些区块,而不是整堆回收。

  1. 其他重要概念
  2. G1是带压缩的收集器,在收集老年代的时候是将存活的对象从一个分区拷贝到另一个分区,拷贝的过程实现了局部的压缩
  3. CSet(Collection Sets): 将要被回收的区块集合。在CSet中存活的对象会在GC过程中移动到另一个可用的分区
  4. Rset(Remembered Sets): 每个区块都有一个 RSet,记录其他 Region 中对象引用本 Region 中对象的关系(如区块 A 中的对象引用了区块 B,区块 B 的 Rset 需要记录这个信息)。RSet的价值在于收集器不需要扫描整个堆谁引用了当前分区中的对象,只需要扫描Rest
  5. SATB(SnapShot-At-The-Beginning): SATB是G1 GC在并发标记阶段采用的增量式的标记算法。并发标记是多线程的,但是同一时刻只扫描一个分区
G1工作流程

G1 收集器主要包括了以下 4 种操作:

  1. 年轻代收集
  2. 并发收集
  3. 混合式垃圾收集(Mixed GC 新生代和老年代的混合回收)
  4. 必要时的 Full GC(应极力避免,也是调优的主要方向)
年轻代收集

年轻代中的垃圾收集流程(Young GC):

可以看到年轻代收集概念上和之前介绍的其他分代收集器差别不大,也是STW的,但是它的年轻代会动态调整。 G1 通过控制年轻代 Region 的个数来控制 YGC 的时间开销。

并发标记周期(global concurrent marking)

这个过程类似CMS,它主要是为了 Mixed GC 提供标记服务,分为四个步骤执行:

  1. 初始标记:STW,标记GC Roots对象。需要注意的是,这个阶段是会发生一次 YGC 的,它的STW也是公用了YGC的暂停时间。
  2. 并发标记:从 GC Roots 开始扫描,对 heap 中对象进行标记,收集各个 Region 中的存活对象信息(采用了SATB算法,是一种三色标记算法)。
  3. 重新标记:STW,标记再并发标记阶段发生变化的对象
  4. 清理:清理阶段,清理空的 Region

到这里,G1 的一个并发周期就算结束了,其实就是主要完成了垃圾定位的工作,定位出了哪些分区是垃圾最多的

三色标记算法和SATB(snapshot-at-the-beginning)

将对象分为三类:

  1. 黑色: 根对象,或者该对象和它的子对象(即它的成员变量)都被扫描过了(对象和它的成员变量都被标记过了)。
  2. 灰色: 对象被扫描,但对象的子对象还没有标记完
  3. 白色: 未被扫描的对象, 扫描完成所有对象后,白色就是不可达对象

工作过程: (1) 根对象被被置为黑色,子对象被置为灰色 (2) 继续由灰色遍历,将已经扫描了的子对象置为黑色 (3) 遍历完了所有可达对象后,所有可达对象变成了黑色,不可达对象还是白色,需要被清理

但是标记过程中,应用程序也再运行,对象的指针可能改变,就会产生对象丢失问题.本来不该被回收的对象,再扫描完成后还是白色。如下

代码语言:javascript
复制
// 初始状态,A引用了B,C对象,B引用了D对象
A->B->D
 ->C
// 运行期间做了改变
A->B
 ->C->D

// 这个时候发生了改变 B.d = null; C.d = D
// 但是这个时候已经将C标记成了黑色,B也是黑色。由于D改变了引用关系没有被标记到还是白色,这样就漏标了

G1 采用 SATB 来解决漏标问题: (1) 开始的时候生成一个快照,标记存活对象 (2) 并发标记的时候所有改变对象入队(把所有旧的引用所指向的对象都变成非白的)

这个算法解决了漏标的问题,但是会产生浮动垃圾,将再下次被收集

混合垃圾回收周期(Mixed GC)

并发周期结束后是混合垃圾回收周期,不仅进行年轻代垃圾收集,而且回收之前标记出来的老年代的垃圾最多的部分区块。Mixed GC 采用复制清除算法,当GC完成后会成功释放空间

关键参数:

  • G1HeapWastePercent: 再 glob concurrent marking 结束后,就可以知道 old 有多少空间要被回收,再每次YGC之后,会检测是否达到这个参数,达到这个参数就触发 Mixed GC
  • G1MixedGCLiveThresHoldPercent: old 中的 region,里面存活对象的占比要再这个参数之下,该 Region 就会被放入 CSet( 也就是垃圾占比高于这个值就会被纳入收集集合)
Full GC

本质上G1是不提供 Full GC 的,它会采用 Serial Old 来收集整个GC Heap。

以下几种会导致 Full GC 的情况,是我们需要极力避免的:

1.concurrent mode failure:并发模式失败,CMS 收集器也有同样的概念。G1 并发标记期间,如果在标记结束前,老年代被填满,G1 会放弃标记。这个时候说明了:

  • 堆需要增加了
  • 或者需要调整并发周期,如增加并发标记的线程数量,让并发标记尽快结束
  • 或者就是更早地进行并发周期,默认是整堆内存的 45% 被占用就开始进行并发周期

2.晋升失败: 跟CMS类似,堆空间垃圾太多导致无法完成年轻代的拷贝到老年代,不得不退化成 Full GC来完成垃圾回收

3.疏散失败:年轻代垃圾收集的时候,如果 Survivor 和 Old 区没有足够的空间容纳所有的存活对象。这种情况肯定是非常致命的,因为基本上已经没有多少空间可以用了,这个时候会触发 Full GC 也是很合理的(这个时候最简单的就是增加堆的大小)

4.大对象分配失败,我们应该尽可能地不创建大对象,尤其是大于一个区块大小的那种对象

G1 相较于 CMS 的优势
  1. 压缩空间方面G1有优势,并且没有什么内存碎片
  2. Eden,Survivor,Old 不固定,内存使用上更加灵活
  3. 可以设置停顿时间来控制垃圾收集时间
  4. G1 可以再 young 区间使用, CMS 只能再 old 区使用
G1参数优化

G1 调优的目标是尽量避免出现 Full GC,其实就是给老年代足够的空间,或相对更多的空间,有以下几点我们可以进行调整的方向:

  • 增加堆大小,或调整老年代和年轻代的比例
  • 增加并发周期的线程数量,其实就是为了加快并发周期快点结束
  • 让并发周期尽早开始,这个是通过设置堆使用占比来调整的(默认 45%)
  • 在混合垃圾回收周期中回收更多的老年代区块

另外 G1 很重要的目标是达到可控的停顿时间

常用的参数:

  • -XX:+UseG1GC(使用G1收集器)
  • -XX:MaxGCPauseMillis=200(指定目标停顿时间,默认值 200 毫秒) 。这个停顿时间并不是设置的越短越好,如果设置的太短就会导致每次收集的CSet越小,垃圾越来越多,最后退化成 Serial 的 Full GC
  • -XX:InitiatingHeapOccupancyPercent=45(整堆使用达到这个比例后,触发并发 GC 周期,默认 45%。降低这个数值,使并发周期提前进行,可以降低晋升失败避免full gc )。这里的堆占比不包括young,但是包括 old+humongous
  • -XX:G1HeapWastePercent,old region中垃圾占比达到这个值就触发Mixed GC
  • -XX:ParallelGCThreads=n(STW期间,并行GC线程数)
  • -XX:ConcGCThreads=n(并发标记阶段,并行执行的线程数,增加这个值可以让并发标记更快完成,如果没有指定这个值,JVM 会通过以下公式计算得到:ConcGCThreads=( ParallelGCThreads + 2) / 4^3)
  • -XX:G1HeapRegionSize=n(每一个 region 的大小,默认值为根据堆大小计算出来,取值 1MB~32MB,这个我们通常指定整堆大小就好了。)
  • -XX:NewRatio=n(老年代/年轻代,默认值 2,即 1/3 的年轻代,2/3 的老年代)
  • -XX:SurvivorRatio=n(Eden/Survivor,默认值 8,这个和其他分代收集器是一样的)
  • -XX:MaxTenuringThreshold =n(从年轻代晋升到老年代的年龄阈值,也是和其他分代收集器一样的)

GC日志

gc 日志参数
  • -XX:+PrintGC: 打印 GC 日志
  • -XX:+PrintGCDetails: 打印详细 GC 日志
  • -XX:PrintHeapAtGC: 在GC前后打印堆信息
  • -XX:+PrintGCTimeStamps: 在每次GC日志前加一个时间戳,标识JVM启动到现在的时间
  • -Xloggc:gc.log: 保存GC日志, 输出到文件中保存起来, 这里文件名是 gc.log
  • -XX:+PrintTenuringDistribution: 打印 survivor 每段年龄的大小
young gc
full gc
文字说明

每一种收集器的日志形式都是由它们自身的实现所决定的,换而言之,每个收集器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性,例如以下两段典型的GC日志:

代码语言:javascript
复制
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]  
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。

GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的。 例如下面这段新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc() 方法所触发的收集,那么在这里将显示“[Full GC (System)”。 [Full GC 283.736: [ParNew: 261599K->261599K(261952K), 0.0000288 secs] 接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。

后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量-> GC后该内存区域已使用容量 (该内存区域总容量)”。而在方括号之外的“3324K->152K( 11904K)”表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆总容量)”。

再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。有的收集器会给出更具体的时间数据,如“[Times: user=0.01 sys=0.00, real=0.02 secs]”,这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。

内存分配规则

对象的内存分配,往大方向上讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地在栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB 上分配。少数情况下也可能会直接分配在老年代中。

有以下几条普遍的内存分配规则

对象优先分配在Eden区域

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

大对象直接进入老年代

所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组(byte[] 数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。

动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

参考资料

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-11-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 判断对象是否存活
    • 引用计数算法(Reference Counting)
      • 可达性分析算法(GC Roots Analysis)
        • finalize()方法最终判定对象是否存活
          • 1)第一次标记并进行一次筛选。
          • 2) 第二次标记
      • GC回收动作
        • Yonug GC
          • Full GC
          • GC算法
            • 标记-清除算法 (Mark-Sweep)
              • 复制算法 (Copying)
                • 标记-整理算法 (Mark-Compact)
                  • 分代收集算法 (Generational Collecting)
                  • 垃圾收集器
                    • Serial收集器
                      • 特点
                    • ParNew收集器
                      • 特点
                    • Parallel Scavenge收集器
                      • 特点
                    • Serial Old(MSC)收集器
                      • Parallel Old收集器
                        • CMS(Concurrent Mark Sweep)收集器
                          • 工作过程
                          • CMS 使用总结
                          • 优化总结
                        • G1(Garbage First)收集器
                          • G1概述
                          • G1工作流程
                          • G1 相较于 CMS 的优势
                          • G1参数优化
                      • GC日志
                        • gc 日志参数
                          • young gc
                            • full gc
                              • 文字说明
                              • 内存分配规则
                                • 对象优先分配在Eden区域
                                  • 大对象直接进入老年代
                                    • 长期存活的对象将进入老年代
                                      • 动态对象年龄判定
                                      • 参考资料
                                      领券
                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档