GC 4 大垃圾收集算法

什么是 GC?(分代收集算法)

GC(Garbage Collection)

  • 次数上频繁手机 Young 区
  • 次数上较少收集 Old 区
  • 基本不动元空间

GC 算法总体概述

在这里插入图片描述
JVM 在进行 GC 时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此 GC 按照回收的区域又分了两种类型,一种是普通 GC(minor GC),一种是全局 GC(major GC or Full GC)

Minor GC 和 Full GC 的区别

  • 普通 GC(Minor GC):只针对新生代区域的 GC,指发生在新生代的垃圾收集动作,因为大多数 Java 对象存活率都不高,所以 Minor GC 非常频繁,一般回收速度也比较快
  • 全局 GC(Major GC or Full GC):指发生在老年代的垃圾收集动作,出现了 Minor GC,经常会伴随着至少一次的 Minor GC(但并不是绝对的)。Major GC 速度一般要比 Minor GC 慢 10 倍以上

引用计数法

在这里插入图片描述
代码示例:

package com.java.demo;

/**
 * @author Woo_home
 * @create 2020/7/9 21:45
 */

public class RefCountGC {

    // 这个成员属性唯一的作用就是占用一点内存
    private byte[] bigSize = new byte[2 * 1024 * 1024];

    Object instance = null;

    public static void main(String[] args){
        RefCountGC objectA = new RefCountGC();
        RefCountGC objectB = new RefCountGC();

		// 相互引用
        objectA.instance = objectB;
        objectB.instance = objectA;
        objectA = null;
        objectB = null;

        // 手动触发 GC
        System.gc();
    }
}

GC 四大算法

复制算法(Copying)

年轻代 中使用的是 Minor GC,这种 GC 算法采用的是复制算法(Copying)

原理

在这里插入图片描述
Minor GC 会把 Eden 中的所有活的对象都移到 Survivor 区域中,如果 Survivor 区中放不下,那么剩下的活的对象就被移到 Old Generation 中,也就是说一旦收集后,Eden 就变成空的了

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


-XX:MaxTenuringThreshold :设置对象在新生代中存活的次数

动态演示

解释

年轻代中的 GC,主要是复制算法(Copying)

在这里插入图片描述

HotSpot JVM 把年轻代分为了三部分:1 个 Eden 区2 个 Survivor 区(分别叫 fromto)。默认比例是 8:1:1 ,一般情况下,新创建的对象都会被分配到 Eden 区(一些大对象特殊处理),这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定的程度时,就会被移动到老年代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在 年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另一块上面。复制算法不会产生内存碎片


在 GC 开始的时候,对象只会存在于 Eden 区和名为 “From”Survivor 区,Survivor“To” 是空的。紧接着进行 GCEden 区中所有存活的对象都会被复制到 “To”,而在 “From” 区中,仍存活的对象会根据它们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过 -XX:MaxTenuringThreshold 来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到 “To” 区域。经过这次 GC 后,Eden 区和 From 区已经被清空,这个时候,“From”“To” 会交换它们的角色,也就是说新的 “To” 就是上次 GC 前的 “From”,新的 “From” 就是上次 GC 前的 “To”。不管怎样,都会保证名为 ToSurvivor 区域是空的。Minor GC 会一直重复这样的过程,直到 “To” 区被填满,“To” 区域被填满之后,会将所有的对象移动到年老代中

劣势

复制算法它的缺点也是相当明显的

  • 1、它浪费了一半的内存

  • 2、如果对象的存活率很高,我们可以极端一点,假设是 100% 存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变得不可忽视。所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服 50% 内存的浪费

标记清除(Mark-Sweep)

老年代 一般是由标记清除或者是标记清除与标记整理的混合实现

原理

垃圾收集算法——标记清除法(Mark-Sweep)

算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象,如下图所示:
在这里插入图片描述
优点:不需要额外空间
缺点:两次扫描,耗时严重。会产生内存碎片

用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被消耗尽的时候,GC 线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记整理工作接下来便让应用程序恢复运行

劣势

  • 1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行 GC 的时候,需要停止应用程序,这会导致用户体验非常差劲
  • 2、其次,主要的缺点则是这种方式清理出来的 空闲内存是不连续的,这点不难理解,可回收的对象都是随即出现在内存的各个角落的,现在把它们清除之后,内存的遍布自然会乱七八糟。而为了应付这一点,JVM 就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找

标记整理算法

标记-清除算法会使内存产生碎片,那么如何解决这个问题呢?很显然,清除以后再清理一下内存不就行了么,然而标记-整理算法并不是简单地清理一下内存

原理

在这里插入图片描述

在整理阶段,不再对标记的对象做回收,而是通过所有存活对象都向一端移动,然后直接清除边界以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址一次排排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新的对象分配内存时,JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销

劣势

标记-整理算法唯一的缺点就是效率也不高,不仅要标记所有的存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记-整理算法要低于复制算法

总结

内存效率

复制算法 > 标记清除算法 > 标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)

内存整齐度

复制算法 = 标记整理算法 > 标记清除算法

内存利用率

标记整理算法 = 标记清除算法 > 复制算法


可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多的内存,而为了尽量兼顾上面所提到的三个指标,标记 / 整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记 / 清除多了一个整理内存的过程

难道没有一种最优算法吗?(分代垃圾算法)

是的,没有最好的算法,只有最合适的算法,也就是 =======> 分代收集算法

年轻代(Young Gen)

年轻代特点是区域相对老年代较小,对象存活率低

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过 HotSpot 中的两个 Survivor 的设计得到缓解

老年代(Tenure Gen)

老年代的特点是区域较大,对象存活率高

这种情况下,存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现

Mark 阶段的开销与存活对象的数量成正比,这点上来说,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核 / 线程利用,对并发、并行的形式提高标记效率

Sweep 阶段的开销与所管理区域的大小形正相关,但 Sweep “就地处决” 的特点,回收的过程没有对象的移动。使其相对其它的对象移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题

Compact 阶段的开销与存活对象的数据成正比,如上一条所描述,对于大量对象的移动是很大开销的,作为老年代的第一选择并不合适

基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以 HotSpot 中的 CMS 回收器为例, CMS 是基于 Mark-Sweep 实现的,对于对象的回收效率很高,而对于碎片问题,CMS 采用基于 Mark-Compact 算法的 Serial Old 回收器作为补偿措施:当内存回收不佳(碎片导致的 Concurrent Mode Failure 时),将采用 Serial Old 执行 Full GC 以达到对老年代的内存的整理

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值