JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器

JVM调优是一层窗户纸,只是看起来很难。学完本节课,让你:
熟悉 GC 常用算法,熟悉常见垃圾回收器,具有实际 JVM 调优实战经验

What is garbage

什么是垃圾?没有引用指向的对象就是垃圾。
JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第1张图片

java和c++垃圾回收的区别
java:
由GC处理垃圾,一般都不是马上回收.GC有自己的想法
开发效率高,执行效率低
c++
代码中手动处理垃圾
可能忘记回收,导致内存泄漏
可能回收多次,导致非法访问并回收了别人正在用的内存
开发效率低,执行效率高

怎么找到垃圾?

引用计数

对象头上有个reference count,上面记着有几个引用,RC为0时这个对象就是垃圾
py就是使用了引用计数
JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第2张图片
缺点:不能解决循环引用
JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第3张图片

根可达算法

何为根对象

  • JVM stacks:线程的栈帧里引用的对象
  • static reference in method area,Clazz:方法区中的静态变量和类结构
  • run-time constant pool:常量池
  • native method stack:本地方法栈(调用C C++方法)用到的对象

从根(Root)上的对象开始,顺着线往下捋,看根对象引用谁,能找到的都是有用的,剩下的就是垃圾
一般这些有用的对象,就称作存活对象.
JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第4张图片

常见的垃圾回收算法 GC Algorithms

  1. 标记清除(Mark-Sweep) - 位置不连续 产生碎片 效率偏低(两遍扫描)
  2. 拷贝算法 (copying) - 没有碎片,浪费空间
  3. 标记压缩(mark compact) - 没有碎片,效率偏低(两遍扫描,指针需要调整)

Mark-Sweep 标记清除算法

概念:
把有用的和没用的对象用标记标出来(RootSearching),然后清除没用的对象

特点:

  1. 算法相对简单,适用于存活对象比较多的情况,因为需要回收的对象很少,
  2. 不适合Eden伊甸区,伊甸区的存活对象比较少
  3. 两遍扫描,效率偏低;(第一遍标记可到达的对象,第二遍清除没用的对象)
  4. 容易产生碎片.
  5. 不浪费空间
  6. 不需移动复制对象

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第5张图片

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第6张图片

Copying 拷贝算法

概念:
把区域分成两半:a区存放对象,b区留着啥也不放
GC时,先从a区找到存活对象,把它拷贝到b区,然后把a区的对象全部回收;
下次回收时从b拷贝到a,然后回收b
特点:

  1. 算法相对简单,适用于存活对象比较少的情况(适合于Eden区)
  2. 只扫描一次,效率较高
  3. 不会产生碎片
  4. 空间浪费,内存减半
  5. 移动复制对象,需要调整对象的引用,效率较低

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第7张图片

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第8张图片

Mark-Compact标记压缩算法

概念:
把存活对象往区域的前边压缩,剩下的全部清除回收
特点

  1. 扫描两次,第一遍标记,第二遍移动压缩
  2. 不会产生碎片
  3. 不浪费空间
  4. 需要移动对象,效率偏低

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第9张图片

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第10张图片

堆内存逻辑分区(针对分代的垃圾回收器)

  • 除了 Epsilon,ZGC,Shenandoah 之外的GC都是逻辑分代模型
  • G1是逻辑分代,物理不分代(物理分代就是内存里确实有这样一块空间)
  • 除此之外,不仅逻辑分代,而且物理分代。

新生代:老年代 = 1:2

// 查看老年代和新生代占用的空间比例
java -XX:+PrintFlagsFinal -version | grep NewRatio

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第11张图片

  • 新生代

    • 新生代存活对象少,使用的是拷贝算法
    • 分为一个伊甸区,两个survivor幸存区
    1. Eden
    2. Survivor-S1
    3. Survivor-S2
      S1和S2一般一起聊,也有人把他俩叫做S0和S1,或者from和to,都一个意思
  • 老年代
    老年代存活对象多,使用的是标记压缩算法,或者标记清除算法

一个对象从出生到消亡

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第12张图片

GC概念

经常听到MinorGC,YGC,FGC等词,他们是啥意思呢?

  • MinorGC/YGC,新生代空间耗尽时触发

  • MajorGC/FullGC/FGC,老年代无法继续分配空间时触发,新生代和老年代同时进行回收,比较慢,重量级.

  • 一般垃圾回收的过程:
    YGC回收之后,Eden中大多数的对象会被回收,活着的进入s0
    再次YGC,活着的对象eden + s0 -> s1
    再次YGC,eden + s1 -> s0
    年龄足够 -> 老年代 (15 CMS 6)
    s区装不下 -> 老年代
    老年代满了FGC Full GC

  • JVM调优指什么?GC Tuning (Generation)
    尽量减少FGC
    MinorGC = YGC
    MajorGC = FGC

栈上分配问题

栈上分配的技术基础:

  1. 逃逸分析:逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。
  2. 标量替换:允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。

直接分配在Eden区的话,会存在多线程的竞争,效率较低.
为了提高效率,减少多线程的竞争,会优先考虑分配在栈上和线程本地 TLAB(ThreadLocal Allocation Buffer)上
什么是TLAB
Java常见面试题—栈分配与TLAB
C语言struct都可以在栈上分配;
为了对标C,Java中的小对象、无逃逸(就在某段代码中使用)、支持标量替换(可以用普通的int等属性代替整个对象)、无需调整这样的对象分配在栈上。
如果栈上分配不下的话,会把它们分配到 线程本地 TLAB(ThreadLocal Allocation Buffer)
线程本地空间是线程独有的,避免多线程的争用,效率较高

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第13张图片
JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第14张图片

小实验验证一下:

package com.mashibing.jvm.c5_gc;
//-XX:-DoEscapeAnalysis -XX:-EliminateAllocations -XX:-UseTLAB -Xlog:c5_gc*
// -代表去掉属性
// -XX:-DoEscapeAnalysis 去掉逃逸分析
// -XX:-EliminateAllocations 去掉标量替换
// -XX:-UseTLAB 去掉线程专有对象分配
public class TestTLAB {
    //User u;  // 如果在这里写User u,方法里面 u=new User(), 这个叫就是有逃逸,因为这个对象被外面的给引用了。
    class User {
        int id;
        String name;

        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }

    void alloc(int i) {
        new User(i, "name " + i); // 逃逸:u=new User()
    }

    public static void main(String[] args) {
        TestTLAB t = new TestTLAB();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000_0000; i++) t.alloc(i);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

对象何时进入老年代

跟年龄有关:

  1. age超过-XX:MaxTenuringThreshold指定次数(TGC)
    对象头,markword里面,GC age标识位占用4位,所以对象的年龄最大为15
    Parallel Scavenge 阈值 15
    CMS 6
    G1 15
  2. 动态年龄
    假设有次的YGC是Eden&S1->S2,如果S2中的存活对象超过了S2空间的一半,
    就把S2中年龄最大的对象放入老年代
    参考资料
  3. 分配担保
    YGC期间 survivor区空间不够了 空间担保直接进入老年代
    参考资料

对象分配过程总结

JVM参数查看

JVM的参数分为三种:

  1. 标准参数,-开头的参数, 所有版本JDK都支持,直接输入java 查看
  2. 非标准参数, -X开头的参数,输入java -X 查看
  3. 不稳定参数 -XX:(+/-), 每个版本可能不同, java -XX:+PrintFlagsFinal 查看,特别多,几百个

常见的垃圾回收器

名词解释:
Serial 单线程
Parallel 多线程
STW:stop-the-world,时间停止!只有我(GC)能动! 即GC时需要暂时停止JVM中的程序指令
safe point安全点:要GC时,程序中很多线程在运行,并不马上stop the world,而是让各个线程都做好准备再去暂停,暂停的点就是安全点.

JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第15张图片
常见组合:

  • Serial+Serial Old
  • Parallel Scavenge+Parallel Old (jdk1.8默认)
  • ParNew+CMS

垃圾收集器跟内存大小的关系
Serial 几十兆
PS 上百兆 - 几个G
CMS - 20G左右
G1 - 上百G
ZGC - 4T - 16T(JDK13)

只要虚线连在一起的,就能进行组合

垃圾回收器的历史:

  1. JDK诞生 Serial追随
  2. 提高效率,诞生了PS,
  3. 为了配合CMS,诞生了ParNew,
  4. CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多,因此目前没有任何一个JDK版本默认是CMS
  5. 并发垃圾回收是因为无法忍受STW

第一种组合:Serial+Serial Old

Serial (年轻代 串行回收)

a stop-the-world,copying collector which uses a single GC thread
STW的,使用coping算法的,单线程的
单CPU效率最高
虚拟机是Client模式的默认垃圾回收器

SerialOld(老年代,串行回收)

a stop-the-world,mark-sweep-compact collector which uses a single GC thread
STW的,使用mark-sweep 或 mark-compact算法的,单线程的

第二种组合:Parallel Scavenge+Parallel Old

Parallel Scavenge (年轻代,并行回收)

a stop-the-world,copying collector which uses multiple GC threads
stw的,使用coping算法的,多线程的

ParallelOld(老年代,并行回收)

a compacting collector which uses multiple GC threads
使用mark-compact算法的,多线程的

第三种组合:ParNew+CMS

ParNew (年轻代 配合CMS的并行回收)

  • a stop-the-world,copying collector which uses multiple GC threads
  • It differs from “Parallel Scavenge” in that it has enhancements(增强) that make it useable with CMS
  • For example,ParNew does the synchronization needed so that it can run during the concurrent phases of CMS
    例如,ParNew执行所需的同步,以便它可以在CMS的并发阶段运行

其实就是Parallel Scavenge 的增强版,可以和CMS配合使用

PS 和 PN区别的延伸阅读

CMS(老年代和工作线程并发回收)重点

CMS:ConcurrentMarkSweep
a mostly concurrent,low-pause collector

以前垃圾回收时其他的工作线程都要暂停,等GC完毕后才继续
JDK1.4后期引入了CMS,开启了并发回收,垃圾回收线程和其他工作线程可以同时进行.

垃圾回收和应用程序同时运行,降低STW的时间(200ms)

算法:三色标记 + Incremental Update

CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定
CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收

想象一下:
PS + PO -> 加内存 换垃圾回收器 -> PN + CMS + SerialOld(几个小时 - 几天的STW)
几十个G的内存,单线程回收 -> G1 + FGC 几十个G -> 上T内存的服务器 ZGC

CMS 分为4个阶段
  1. 初始标记:标记根对象,需要STW,因为初始的垃圾并不多,因此耗费的时间不长
  2. 并发标记:垃圾回收线程和工作线程同时执行。一边产生垃圾,一边标记
  3. 重新标记:对在并发标记的过程中新产生的垃圾进行重新标记,或者原来被标记的垃圾变为不是垃圾。需要STW,因为新产生的垃圾不多,所以时间也不是很长
  4. 并发清理:清理的过程也会产生新的垃圾“浮动垃圾”,需要等下一次CMS重新运行的时候再次清理JVM第六课:JVM调优必备理论知识 - 3种垃圾清除算法,常见的垃圾回收器_第16张图片
CMS的问题
  1. Memory Fragmentation 内存碎片问题: 因为标记清除会产生碎片化,如果老年代已经没有地方可以装了,CMS会请出Serial Old让它来进行清理,Serial Old是单线程的,效率很低

    -XX:+UseCMSCompactAtFullCollection
    -XX:CMSFullGCsBeforeCompaction 默认为0 指的是经过多少次FGC才进行压缩

  2. Floating Garbage 浮动垃圾问题:老年代满了,浮动垃圾没有清理完。这时会请出Serial Old让它来进行清理

    Concurrent Mode Failure
    产生:if the concurrent collector is unable to finish reclaiming the unreachable objects before the tenured generation fills up, or if an allocation cannot be satisfiedwith the available free space blocks in the tenured generation, then theapplication is paused and the collection is completed with all the applicationthreads stopped
    这个现象在日志里打印出来是PromotionFailed

以上两个问题的解决方案类似:降低触发CMS的阈值,保持老年代有足够的空间。
–XX:CMSInitiatingOccupancyFraction 92 可以理解为,老年代内存到达92% 的时候,触发CMS垃圾回收期。
可以降低这个值,让CMS保持老年代足够的空间

可以使用命令查看默认值:

java -XX:+PrintFlagsFinal -version | grep CMSInitiatingOccupancyFraction

这个值在1.8和11.0.3版本默认都是-1;

G1(10ms)

算法:三色标记 + SATB

ZGC (1ms) PK C++

算法:ColoredPointers + LoadBarrier(读屏障)

Shenandoah

算法:ColoredPointers + WriteBarrier(写屏障)

常见垃圾回收器组合参数设定:(1.8)

  • -XX:+UseSerialGC = Serial New (DefNew) + Serial Old
    小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器

  • -XX:+UseParNewGC = ParNew + SerialOld
    这个组合已经很少用(在某些版本中已经废弃)
    原因

  • -XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old

  • -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】

  • -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old

  • -XX:+UseG1GC = G1

  • Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC
    java +XX:+PrintCommandLineFlags -version
    通过GC的日志来分辨

  • Linux下1.8版本默认的垃圾回收器到底是什么?
    1.8.0_181 默认(看不出来)Copy MarkCompact
    1.8.0_222 默认 PS + PO

你可能感兴趣的:(JVM,算法,java,开发语言)