基础篇 jvm的垃圾回收器

284 阅读13分钟

GC知识点脉络

什么对象算垃圾?

  1. 对象存活判断

何时回收垃圾?

  1. 内存分代模型
  2. 垃圾回收策略
  3. 对象分配流程

怎么回收垃圾?

  1. GC算法
  2. 垃圾回收器

对象存活判断

Reference Count

引用计数算法,有一个地方引用这个对象,计数器加1,引用失效减1,当计数器为0,对象就不再被用

缺点:解决不了循环引用

Root Searching

根可达算法,通过一系列的GC Roots对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的,会被判定是可回收对象。例如对象循环引用,但是他们到GC Roots不可达,所以可回收

可做GC Roots的对象如下:

  • JVM stack,虚拟机栈(栈帧中的本地变量表)中引用的对象
  • static references in method,方法区中的类静态变量
  • runtime constant pool,方法区中常量池引用的对象
  • native method stack,本地方法引用的对象
  • class类对象

程序启动之后马上需要的对象就是根对象

一句话总结:对象被方法局部变量类静态变量引用,就是GC Roots,不会被回收

GC参数JVM.png

  • 新生代 + 老年代 + 永久代(1.7)/ 元数据区(1.8)

  • 新生代 : 老年代 1:3 / 1:2

  • 新生代

    Eden : Survivor : Survivor 8:1:1

  • 老年代

    老年代空间耗尽触发Major GC/Full GC,新生代老年代同时回收

  • 永久代/元数据区

    因为永久代(1.7)不会被GC,所以必须指定大小限制,否则可能会OOM

    元数据区(1.8)可设可不设,不设最大就是物理内存

    字符串常量1.7在永久代,1.8移到堆中

垃圾回收策略

两条线:YGC前和YGC时

GC策略.jpg

Young GC/Minor GC

触发YGC时机
  • 往Young区分配对象时空间不够

使用Copying算法

YGC流程

将Eden区和S0区的对象进行一次Root Searching,找出活跃对象,复制到S1区,并将Eden区和S0区中的不可达对象清空,交换S0与S1指针

若S1不足以容纳Eden和另一个S0中的存活对象,会使用分配担保机制将多余的对象将被移到老年代,称为过早提升(Premature Promotion) ,导致老年代中短期存活对象的增长,可能会引发严重的性能问题

Minor GC之后存活对象一般不超过10%,所以Eden和S0S1的比是8:1:1

JVM会给每个对象定义一个对象年龄(Age)计数器,记在对象头里,对象在Survivor区中每熬过一次Minor GC,年龄就加1。待到年龄到达一定岁数(默认是15岁,因为对象头只有4位用来保存对象当前年龄,可通过 -XX:MaxTenuringThreshold参数设置回收年龄),虚拟机就会将对象移动到老年代。

为什么用4位使最大值为15岁,权衡的依据是什么?
Major GC和Full GC不一样吧?

Full GC/Old GC/Major GC

FGC是对Young区和Old一起GC,OGC只对Old区进行GC

触发FGC时机
  • YGC前,Old区可用空间 < Young区全部对象大小,且分配担保没开启
  • YGC前,Old区可用空间 < 历次YGC后进入Old区对象的平均大小
  • YGC时,分配担保失败
  • CMS触发FGC时机

使用Mark-Compact算法

每次Minor GC之前,JVM会检查Old区可用空间,是否大于Young区所有对象总大小,避免出现Minor GC之后所有对象都没被回收的极端情况

若发现Minor GC之后要进入Old区的对象太多,就提前触发Full GC

若Old可用空间大于之前每次Minor GC后进入Old对象的平均大小,尝试进行Minor GC

若剩余存活对象大于Survivor区,小于Old区可用空间,就直接进入Old区,这就是分配担保机制

若剩余存活对象大于Old区可用空间,就触发Full GC,回收Old区和Young区

若Full GC之后剩余存活对象还是大于Old区可用空间,就OOM了

减少FGC的过程就是让Survivor区放得下存活对象,不能触发动态年龄和分配担保进入Old区

如何做到不FGC?

JVM优化总结起来两句话:

  • YGC前YGC时尽量避免让太多对象进入Old区,尽量让对象在Young区分配和回收,避免FGC,或者降低FGC次数
  • 给JVM足够的内存,避免频繁YGC

结合系统运行时内存占用情况,GC后对象存活情况,根据GC策略流程,合理分配Young区(Eden、S0、S1),Old区大小,合理设置一些参数

对象分配流程

对象分配流程.png

(图2)

new对象先判断能否栈上分配,能分配以后就不会被GC,对象直接出栈即释放内存

在什么栈上分配对象?

不能分配则判断是否大于虚拟机参数指定大小,超过了直接分配Old区

不是大对象则判断是否能进TLAB,不能就进Eden区(TLAB也在Eden区)

若Eden不够空间分配内存时,Eden进行一次Minor GC,将对象放进Eden

之后对象会被一次或多次Minor GC,会被移动到S0/S1/Old区,直到被GC掉

什么对象会往栈上放?

线程私有小对象

无逃逸(只在特定代码块中有效,比如new出来没人引用他)

标量替换:可用基本类型代替的对象 (-XX:-/+EliminateAllocations)

什么对象会往TLAB分配?

线程本地分配TLAB(Thread Local Allocation Buffer)(-XX:-/+UseTLAB)

占用1%Eden,每个线程独有

多线程时不用竞争Eden就可申请空间,提高效率

小对象

什么对象会直接进入Old区?

  • 躲过15次GC

  • 大对象

    设置参数 -XX:PretenureSizeThreshold,大于等于这个值的对象直接进入Old区

    避免在Young区中来回复制,耗费时间

  • 动态年龄判断进入

    www.jianshu.com/p/989d3b06a…

    YGC时,若发现Survivor中年龄1+年龄2+....+年龄n的所有对象大小超过Survivor区的50%,就会把年龄n以上的对象放入老年代

  • 分配担保机制进入

GC算法

Mark-Sweep(标记清除)

先标记出所有需要回收的对象,标记完后再统一回收掉

优点:算法简单,适用于存活对象较多的情况,清理过程效率较高

缺点:扫描两遍(第一遍找有用的对象,第二找没用的清除掉)扫描,算法本身效率偏低,会产生大量不连续内存碎片,会导致分配大对象是无法找到足够的连续内存而提前触发另一次GC

Copying(拷贝)

将可用内存五五开分两块,每次只使用其中的一块。当这一块内存用完,就把存活的对象复制到另一块上面,然后再把已经使用过的内存一次性清理掉。

优点:分配对象内存只需移动堆顶指针,按顺序分配内存,只扫描一次,不产生内存碎片,效率高,适用于存活对象较少的Eden区

缺点:空间代价高,需调整对象引用

Mark-Compact(标记整理)

标记过程和Mark-Sweep一样,但后续步骤是把存活对象都向一端移动,然后直接清理掉端边界以外的内存

优点:不会产生碎片,方便对象分配

缺点:扫描两次,需调整对象引用,效率低

垃圾回收器

垃圾回收器组合

Serial + Serial Old

Parallel Scavenge + Paraller Old(PS+PO,JVM默认组合)

ParNew + CMS(吞吐量比G1高)

G1(响应时间比PA+CMS好)

红线连的都能组合

左边6个逻辑上和物理上都分年轻代老年代,右边几个只逻辑上分年轻代老年代

Serial追随JDK诞生,为了提高效率,诞生了PS,为了配合CMS,诞生了PN,CMS在1.4后期引入,是里程碑式的GC,开启了并发回收的过程,但是毛病较多,因此目前没有任何JDK版本默认CMS

Serial

stop-the-world(STW)

单线程,年轻代串行回收,所有线程在safe point停止(比如还没unlock就等解锁之后再停止)

单机CPU效率最高,现代计算机内存空间大,故此法会导致停顿时间长,极少用

Serial Old

Serial,用Mark-Sweep/Mark-Compact算法,在老年代清理

Parallel Scavenge

多线程年轻代并行回收

Parallel Old

多线程老年代并行回收,使用Mark-Compact

ParNew(Parallel New)

年轻代并行回收

Parallel Scavenge的变种,对Parallel Scavenge进行增强,以便更好地和CMS配合使用

JVM参数:-XX:+UseParNewGC

GC线程数默认与CPU核心数相同,一个线程一个核心

-XX:ParallelGCThreads参数指定GC线程数(一般不修改)

CMS(Concurrent Mark Sweep)

老年代垃圾回收器

随着服务器内存变大,Serial和Parallel清理的耗时变得无法忍受

故诞生了CMS,这是里程碑式的GC,开启了并发回收的过程,即工作线程和垃圾回收线程同时进行

Parallel是并行回收,多个线程同时回收

CMS是 回收的同时还可以产生新垃圾

CMS清理过程
  • 初始标记(STW)

    找到并标记GC Roots,对象不多,用时短

  • 并发标记

    80%的时间都用在这里,和工作线程同时进行,最耗时

  • 重新标记(STW)

    重新标记并发标记时新产生的少量垃圾、以及被并发标记过但现在又不是垃圾的对象

  • 并发清理

    清理垃圾, 并发清理产生的新垃圾称为浮动垃圾,等下一次CMS清理

CMS的问题
  • 占用CPU

    并发标记和清理时,与工作线程同时进行,但耗时较高

    CMS默认启动GC线程数是:(CPU核心数 + 3) / 4,即4核CPU就会占用1个核进行GC

    导致占用了一部分工作线程的运算能力,降低总吞吐量

  • 浮动垃圾 & Concurrent Mode Failure

    由于并发清理时工作线程是运行的,所以Old区会预留一部分空间来分配新对象,不能像其他收集器那样等到老年代几乎完全被填满了再进行收集

    可通过调整 -XX:CMSInitiatingOccupancyFraction 参数(默认92%,后来改68%这里要确认哪个版本开始68%)降低触发FGC的阈值(老年代已使用空间比例),要注意调太低也会浪费内存

    这又导致两个问题:

    • 并发清理期间可能会产生从Young区过来的对象,没人引用之后就会成为浮动垃圾,只能等下次CMS清理
    • 若在并发清理期间,Old区预留空间不够分配给从Young区过来的对象,会发生Concurrent Mode Failure,此时会使用Serial Old代替CMS,强行STW进行GC,极其耗时,可能导致机器卡死
  • 内存碎片

    CMS使用标记清理算法,会产生大量内存碎片,可能会导致Young过来的对象找不到连续的内存空间而频繁触发FGC

    所以CMS会在指定次数的FGC结束之后,STW进行一次碎片整理(有参数可配置,但JDK9之后废弃了)

CMS触发FGC时机
  • 内存碎片过多,导致Young过来的对象找不到连续的内存空间
  • 发生Concurrent Mode Failure

G1(Garbage First)

  • 将内存分为多个region,每个大小=堆内存/2048
  • Young区和Old区由一个个region组合而成,总大小根据对象分配和回收情况动态变化,没有传统的那种固定空间
  • Young区大小会变,创建对象时会不断增加region,直到最大比例60%
  • region在对象分配时会属于Young区、Old区和H区,对象被回收后就成为空闲region,直到下次被分配对象
  • 还是有Eden区、S0S1区、Old区的概念,对象分配流程和垃圾回收策略跟传统的基本类似,-XX:SurvivorRatio=8还能用
  • Young区达到最大比例(默认60%)就会触发YGC
  • G1多了一个H区,大对象不直接进入Old区,会进入H区
  • 垃圾回收过程使用复制算法
  • -XX:MaxGCPauseMills设置期望GC停顿时间
  • G1是动态灵活的,会根据预设GC停顿时间,给Young区分配region,到一定程度触发GC,回收时把停顿时间控制在预设范围内,避免一次性回收过多region导致停顿时间过长,或者追求短停顿时间导致频繁GC
  • mixed gc:Old区在堆中占比超过45%触发
  • 避免触发mixed gc:核心是调整参数-XX:MaxGCPauseMills,与之前的回收器一样,尽量避免对象过快进入Old区,避免频繁触发mixed gc
  • 适合大内存机器,解决大内存GC时间过长

内存区域分一个个region,没有物理分代,有逻辑分代,region可以是Old,Survivor,Eden,H区,且不固定,可手动指定region大小

H区即Humongous,大对象(大于region50%)占一个或多个region

新生代空间比例:5% - 60%(上下限可调)

g1会自动根据stw时间自动调整优化新老年代比例,故最好不要手工指定新老年代比例

卡表(是什么?)卡表和三色标记算法貌似不是G1独有,看《深入》

CSet(Colletction Set)

把region分为一个个card,引用的对象所在card被记为dirty

回收垃圾最多、存活对象对少的card,并把放进CSet

回收是到cset找垃圾

RSet(Remember Set)

在每个region记一个hashmap,保存本region里所有对象被别的region里的哪些对象引用说法可能有误

高效回收的关键,空间换时间

由于RSet的存在, 每次给对象赋引用的时候,就得在RSet做额外的记录,称为GC中的写屏障,不等于内存屏障

GC阶段
  • YGC

    Eden空间不足

    多线程并行执行不理解

  • MixedGC(约等于CMS的GC)

    到达阈值触发

    前三步同CMS,最后一步是筛选回收

    YGC和MixedGC有时会穿插进行:YGC进行一半,就开始MixedGC的步骤
    
  • FGC

    Old空间不足

    调用System.gc()

    10以前是串行FGC,之后是并行,所以1.8使用G1尽量不要产生FGC

G1特点
  • 并发收集
  • 压缩空间不会延长GC暂停时间
  • 更易预测的GC暂停时间
  • 适用吞吐量要求不高,响应时间需要特别快的场景
三色标记

黑色 -- 自身和成员变量均已标记完成

灰色 -- 自身被标记,成员变量未被标记

白色 -- 未被标记的对象

漏标情况:本来是存活对象,由于没被遍历到,被当成垃圾回收掉了。128-0203

例如:在并发标记过程中,灰不指向白了,但黑指向白

解决漏标

  • incremental update -- 增量更新算法:关注黑色对象引用的增加,将其重新标记为灰色,下次重新扫描属性。CMS用
  • SATB -- 原始快照算法:关注引用的删除,当灰色对象指向白色的引用消失,要把白色引用推到GC堆栈(GC有个栈用来保存引用),保证白色对象能被GC扫描到 G1使用SATB,因为第一种,黑变灰了,还要重新扫描一遍灰对象,效率低。SATB直接扫描栈里放的引用即可
G1产生FGC怎么办
  • 扩内存
  • 升级CPU(回收快,业务产生对象的速度固定,垃圾回收越快,内存空间越大)
  • 降低MixedGC触发的阈值,让MixedGC提早发生(默认45%)MixedGC可以看做CMS MixedGC:G1的正常回收过程,混合回收,不分区,哪个region满了就收哪个,阈值就是对象占堆空间的值,参数:XX:InitiatingHeapOccupacyPercent,到阈值之前是YGC

垃圾收集器跟内存大小的关系

  • Serial:几十兆
  • PS:上百兆-几个G
  • CMS:20G
  • G1:上百G
  • ZGC:4T

没有完美的GC,只能在特定的情况下选择合适的解决方案