6.垃圾回收算法详解、7.垃圾收集器全解

JVM概述与类的加载机制
JVM 内存模型
对象逃逸分析、JVM 内存分配和回收策略
垃圾回收算法详解、垃圾收集器全解
JVM 调优

6 垃圾回收算法详解

6.1 引用计数算法

在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。在 java 领域一些主流虚拟机没有选用引用计数算法来进行内存管理。那么到底是因为什么呢?我们来往下看:

public class ReferenceCountingGC {
    private Object instance;
    public static void main(String[] args) {
        ReferenceCountingGC obj1 = new ReferenceCountingGC();
        ReferenceCountingGC obj2 = new ReferenceCountingGC();
        obj1.instance = obj2;
        obj2.instance = obj1;
        obj1 = null;
        obj2 = null;
        System.gc();
    }
}

如果上面代码这种情况,进行 GC 时使用引用计数算法,那么变量 obj1、obj2 的计数器值永远不为 0,就不会被收集。

6.2 可达性分析算法

这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。

在 Java 技术体系里面,固定可作为 GC Roots 的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized 关键字)持有的对象。
  • 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。

如下图所示,对象 object 5、object 6、object 7 虽然互有关联,但是它们到 GC Roots 是不可达的, 因此它们将会被判定为可回收的对象,这些对象就会被标记,进行后续处理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s6S5cppT-1611564912956)(https://uploader.shimo.im/f/TXozJXteo1wivRME.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

6.3 判断对象可以被回收

6.3.1 引用类型

java 的引用类型一般分为 4 种:强引用、软引用、弱引用,虚引用

  • 强引用:普通的变量引用
  • 软引用:将对象用 SoftReference 软引用类型的的对象包裹,正常情况下不会被回收。但是,GC 之后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现对内存敏感度不高的高速缓存。
public static SoftReference<ShiroUser> user = new SoftReference<>(new ShiroUser());
  • 弱引用:将对象用 WeakReference 弱类型的对象包裹,弱引用跟没引用差不多,GC 直接回收掉,很少用。
public static WeakReference<ShiroUser> user = new WeakReference<>(new ShiroUser());
  • 虚引用:虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用关系,几乎不用;。

6.3.2 finalize 方法最终判定

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

1.第一次标记:对象在可达性分析后发现没有与 GC Roots 相连接的引用链,会被第一次标记。

2.进行一次筛选:筛选的条件是此对象是 否有必要执行 finalize()方法。‘

2.1 没必要直接被回收:假如对象没有覆盖 finalize()方法,或者 finalize()方法已经被虚拟机调用 过,那么虚拟机将这两种情况都视为“没有必要执行”。

2.2 放在 F-Queue 队列中:假如这个对象被判定为有必要执行 finalize()方法,则会被放在该队列中。

3.第二次标记:稍后收集器将对 F-Queue 中的对象进行第二次小规模的标记。

3.1 如果对象在 finalize()中成功拯救自己(把自己与引用链上任何一个对象建立关联),在第二次标记时就会被移出即将被回收的集合。

3.2 如果此时还么与引用链上的对象关联,基本上就要被回收了。

如下代码:

//VM: -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
// -XX:HeapDumpPath=D:\jvm.dump
public static void main(String[] args) {
    List<Object> list = new ArrayList<>();
    int i = 0;
    int j = 0;
    while (i<10001){
        list.add(new User(i++, Long.toString(System.currentTimeMillis()))); // 关联了 list,不会被回收
        new User(j--, Long.toString(System.currentTimeMillis()));  // 会被回收,怎么拯救呢
    }
}
public class User {
    private Integer id;
    private String name;
    public User(){}
    public User(Integer id, String name){
        super();
        this.id = id;
        this.name = name;
    }
    @Override
    protected void finalize() throws Throwable {
        OOMTest.list1.add(this);
        System.out.println("关闭资源,user" + id + "即将被回收");
    }
public class User {
    private Integer id;
    private String name;
    public User(){}
    public User(Integer id, String name){
        super();
        this.id = id;
        this.name = name;
    }
    @Override
    protected void finalize() throws Throwable {
        OOMTest.list1.add(this);
        System.out.println("关闭资源,user" + id + "即将被回收");
    }

User 类如下:

public class User {
    private Integer id;
    private String name;
    public User(){}
    public User(Integer id, String name){
        super();
        this.id = id;
        this.name = name;
    }
    // get,set 方法略

我们在User类里面重写下finalize()方法并且把当前对象与调用链相关联,就可以让该对象“惨遭一死”了。修改后代码如下:

public class OOMTest {
    public static List<Object> list1 = new ArrayList<>();
    //VM: -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
    // -XX:HeapDumpPath=D:\jvm.dump
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        int i = 0;
        int j = 0;
        while (i<10001){
            list.add(new User(i++, Long.toString(System.currentTimeMillis()))); // 关联了 list,不会被回收
            new User(j--, Long.toString(System.currentTimeMillis()));  // 会被回收,怎么拯救呢
        }
    }
}

User 类如下:

public class User {
    private Integer id;
    private String name;
    public User(){}
    public User(Integer id, String name){
        super();
        this.id = id;
        this.name = name;
    }
    // 重写 finalize()方法
    @Override
    protected void finalize() throws Throwable {
        OOMTest.list1.add(this); // 与引用链关联
        System.out.println("关闭资源,user" + id + "即将被回收");
    }
    
    // get,set 方法略

6.3.3 判断类是否可以被回收

类需要同时满足 3 个条件才能算是“无用的类”:

  • 该类所有的实例都已经被回收
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过放射访问该类的方法。

6.4 垃圾收集算法

6.4.1 标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。

回收过程示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BeoJtMeJ-1611564912959)(https://uploader.shimo.im/f/CIrVGrtJ1LfAO0CN.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

问题:

效率问题

空间问题(标记清除后会产生大量不连续的碎片)

6.4.2 复制算法

复制算法是将内存分为大小相同的两块,每次只用其中一块,当这一块用完了,就将还存活的对象复制到另外一块上面,让后把已经使用过的内存空间一次性清理掉。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QSisaliH-1611564912961)(https://uploader.shimo.im/f/CfweqSLFhFY5XCBp.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

效率低

不会产生不连续的空间

6.4.3 标记-整理算法

根据老年代的特点,有人提出了另外一种标记-整理算法,过程与标记清除算法一致,不过不是直接对可回收对象进行清理,而是让所有存活对象都想一端移动,让后清理掉边界以外的内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wB0AOr2X-1611564912963)(https://uploader.shimo.im/f/oMtqqB5XxD7VbsFZ.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

6.4.4 分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

  • 比如在新生代中,每次收集都会有大量对象(近 99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集;
  • 而老年代的对象存活几率是比较高的,而且没有额外的空间为他们进行分配担保,所以我们必须选择“标记-清除”算法或“标记-整理”算法进行垃圾收集。

7 垃圾收集器全解

7.1 垃圾收集器概述

如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-43WnD38s-1611564912964)(https://uploader.shimo.im/f/XiuiscgH63N8x4HG.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

上图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配 使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器

7.2 Serial 收集器

VM:-XX:+UseSerialGC -XX:+UseSerialOldGC

serial(串行)收集器是一个单线程收集器,单线程一方面意味着只会使用一个 CPU 或者一条线程完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作(“Stop The Word"),直到它收集结束为止。
下图示意了 Serial/Serial Old 收 集器的运行过程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GhO93CVy-1611564912965)(https://uploader.shimo.im/f/HZxKCHFmPpfoohsu.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

耽误其他线程任务

如果服务器是多核,该收集器也是单线程去处理,浪费资源。

7.3 ParNew 收集器

VM: -XX:+UseParNewGC

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和 Serial 收集器完全一样。默认的收集线程数量跟 CPU 核数相同,当然也可以用参数(
-XX:ParallelGCThreads)指定垃圾收集线程数量,一般不推荐修改。

收集器的工作过程如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cI6Waibr-1611564912966)(https://uploader.shimo.im/f/9UeHV56ELNLOfJpk.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

7.4 Scavenge 收集器

Parallerl Scavenge收集器,VM参数如下:

-XX:+UseParallelGC(年轻代)
-XX:+UseParallelOldGC(老年代)
  • Paraller Scavenge 收集器类似于 ParNew 收集器,是 server 模式(内存大于 2G,2 个 CPU)下的默认收集器。
  • Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾收集时间)

下图为该收集器的运行过程示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NrzuMT3x-1611564912966)(https://uploader.shimo.im/f/IvvlUuobzO7pxaj9.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

7.5 CMS 垃圾收集器

7.5.1 运行过程

VM 参数:-XX:+UseMarkSweepGC(老年代)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。他非常符合在注重用户体验的应用上使用,它是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次出现了让垃圾收集线程与用户线程同时(基本上)工作。
从名字(包含“Mark Sweep”)上就可以看出 CMS 收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

1)初始标记(CMS initial mark)

2)并发标记(CMS concurrent mark)

3)重新标记(CMS remark)

4)并发清除(CMS concurrent sweep)

Concurrent Mark Sweep 收集器运行示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8cAG4hv6-1611564912967)(https://uploader.shimo.im/f/djPCKNGVCgzPMfai.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

7.5.2 优缺点

优缺点如下:

优点:并发收集,低停顿。

缺点:

  • 对 CPU 资源敏感(会和服务抢资源)
  • 无法处理浮动垃圾(在并发清理阶段又产生垃圾,这种浮动垃圾只能等下一次 GC 再清理了)
  • 它使用的回收算法“标记-清除”算法会导致收集结束时,会有大量空间碎片的产生,当然通过参数:-XX:+UseCMSCompactAtFullConllection 可以让 JVM 在执行完“标记-清除”后再整理。
  • 执行过程中不确定性,会存在上一次的垃圾回收还没执行完,然后垃圾回收又被出发的情况,特别是在并发标记和并发清理阶段出现,一边回收,系统一边运行,也许还没回收完就再次触发 Full GC,也就是“concurrent mode failure",此时会进入 stop the word,用 Serial Old 垃圾回收器来回收。

7.5.3 相关参数

-XX:+UseConcMarkSweepGC  ——启用CMS
-XX:ConcGCThreads  ——并发的GC线程数
-XX:+UseCMSCompactAtFullCollection  ——Full GC后做压缩整理(减少碎片)
-XX:CMSFullGCsBeforeCompaction ——多少次GC之后压缩一次,默认是0,代表每次GC后都会压缩一次
-XX:CMSInitiatingOccupancyFraction  ——当老年代使用达到该比例时会触发Full GC(默认是92,这是百分比)
-XX:+UseCMSInitiatingOccupancyOnly  ——只使用设定的回收阈值(
-XX:CMSInitiatingOccupancyFraction 设定的值),如果不指定,JVM 会在第一次使用设定值,后续则会自动调整
-XX:+CMSSsacengeBeforeRemark  ——在CMS GC前启动一次minor GC,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的耗时80%都在标记阶段。

7.6 G1 垃圾收集器

VM 参数:-XX:+UseG1GC

G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大量内存的机器,以及高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

7.6.1 G1 垃圾收集器分区

内存各分代不再连续,把整个堆内存分为 2048 块,每块称为一个区(region)。每个区的大小为堆内存大小/2048。内存分布如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vRuyyqCB-1611564912967)(https://uploader.shimo.im/f/iFgmvxSNZmTnLZ8V.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

其中:Humongous 是存放大对象的,如果一个对象过大,可以用多个连续的 Humongous 去存放。

7.6.2 G1 收集器运行过程

G1 收集器一次 GC 的运行过程大致分为以下几个步骤:

  • 初始标记(inital mark, STW):暂停所有的其他线程,并记录下 GC Roots 直接引用的对象,速度很快。
  • 并发标记(Concurrent Marking):同 CMS 的并发标记
  • 最终标记(Remark,STW):同 CMS 的重新标记
  • 筛选回收(Clearup, STW):筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间(可以用 jvm 参数:-XX:MaxGCPauseMillis 指定,默认 200ms)来制定回收计划。
    在这里插入图片描述

7.6.3 G1 垃圾收集器的分类

YoungGC

YoungGC 并不是说现有的 Eden 区放满了马上就会触发,G1 会计算下现在 Eden 区回收大概需要多久时间,如果回收时间远远小于参数:-XX:+MaxGCPauseMillis 设定的值,那么增加年轻代的 region,继续给新对象存放,不会马上做 YoungGC,直到下一次 Eden 区放满,G1 计算回收时间接近参数-XX:+MaxGCPauseMillis 设定的值,那么就会触发 YoungGC.

MixedGC

不是 FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的 Young 和部分 Old(根据期望的 GC 停顿时间确定 Old 区垃圾收集的优先顺序)以及大对象区。

FullGC

停止系统程序,然后采用单线程进行标记,清理和压缩整理,好空闲出来一批 region 来供下一次 MixedGC 使用,这个过程非常耗时。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值