厚积薄发打卡Day64 :【狂神 & bugstack】狂神JVM快速入门(下)<GC机制与算法>

视频学习:【狂神说Java】JVM快速入门篇

相关资料:

  1. 《深入理解Java虚拟机》第三版
  2. 漫画:什么是JVM的垃圾回收?
    • 非常简单易懂,十分推荐
  3. bugstack虫洞栈——面经#27
    • 实例查看回收过程

10. 实例验证GC

老是说GC,如何看到GC运行的实际效果?TALK IS CHEAP,SHOW ME THE CODE

举个例子:(例子来源:bugstack虫洞栈——面经#27

测试代码:

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) throws InterruptedException {
        testGC();
    }
    public static void testGC() throws InterruptedException {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 假设在这行发生GC, objA和objB是否能被回收?
        //这里我们先采用 jvm 工具指令,jstat来监控。因为监控的过程需要我手敲代码,比较耗时,所以我们在调用testGC()前,睡眠会
        //  Thread.sleep(10000);
        System.gc();
    }
}

在启动的程序中,加入GC打印参数,观察GC变化结果。

-XX:+PrintGCDetails  #打印每次gc的回收情况 程序运行结束后打印堆空间内存信息(包含内存溢出的情况)
-XX:+PrintHeapAtGC  #打印每次gc前后的内存情况
-XX:+PrintGCTimeStamps #打印每次gc的间隔的时间戳 full gc为每次对新生代老年代以及整个空间做统一的回收 系统中应该尽量避免
-XX:+TraceClassLoading  #打印类加载情况
-XX:+PrintClassHistogram #打印每个类的实例的内存占用情况
-Xloggc:/Users/xiaofuge/Desktop/logs/log.log  #配合上面的使用将上面的日志打印到指定文件
-XX:HeapDumpOnOutOfMemoryError #发生内存溢出将堆信息转存起来 以便分析

在这里插入图片描述

  • -XX:+PrintGCDetails

    [GC (System.gc()) [PSYoungGen: 6717K->712K(37888K)] 6717K->720K(123904K), 0.0020003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC (System.gc()) [PSYoungGen: 712K->0K(37888K)] [ParOldGen: 8K->590K(86016K)] 720K->590K(123904K), [Metaspace: 3105K->3105K(1056768K)], 0.0048082 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    Heap
     PSYoungGen     total 37888K, used 983K [0x00000000d6200000, 0x00000000d8c00000, 0x0000000100000000)
      eden space       32768K,   3% used [0x00000000d6200000,0x00000000d62f5db8,0x00000000d8200000)
      from space       5120K,    0% used [0x00000000d8200000,0x00000000d8200000,0x00000000d8700000)
      to  space        5120K,    0% used [0x00000000d8700000,0x00000000d8700000,0x00000000d8c00000)
     ParOldGen      total 86016K, used 590K [0x0000000082600000, 0x0000000087a00000, 0x00000000d6200000)
      obje space       86016K,   0% used [0x0000000082600000,0x0000000082693820,0x0000000087a00000)
     Metaspace       used 3128K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 343K,   capacity 388K, committed 512K, reserved 1048576K
    
    • 怎么看结果?
      • PSYoungGen:GC日志中的PSYoungGen(PS是指Parallel Scavenge)为Eden+FromSpace,而整个YoungGeneration为Eden+FromSpace+ToSpace。
      • ParOldGen:ParOldGen表示gc回收前后老年代的内存变化
      • MetaSpace:JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
    • 从运行结果可以看出内存回收日志,Full GC 进行了回收。
    • 也可以看出JVM并不是依赖引用计数器的方式,判断对象是否存活。否则他们就不会被回收啦

11. GC主要算法

漫画:什么是JVM的垃圾回收?

  • 如何判断哪些垃圾是需要回收的?
  • 有哪些重要的垃圾回收算法?

接下来一个个进行回答:这些问题的关键其实就是有关GC的四大算法

11.1 如何判断哪些垃圾是需要回收的:

引用计数算法

定义

  • 它通过记录对象被引用的次数从而判断该对象的重要程度;
    • 如果该对象被其它对象引用,则它的引用计数加一,
    • 如果删除对该对象的引用,那么它的引用计数就减一,
    • 当该对象的引用计数为0时,那么该对象就会被回收。

评价

  • 从实现来看,引用计数器法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但是它的实现方案简单,判断效率高,是一个不错的算法。
    • 也有一些比较出名的引用案例,比如:微软COM(Component Object Model) 技术、使用ActionScript 3的FlashPlayer、 Python语言等。
  • 但是,在主流的Java虚拟机中并没有选用引用技术算法来管理内存,主要是因为这个简单的计数方式在处理一些相互依赖、循环引用等就会非常复杂。可能会存在不再使用但又不能回收的内存,造成内存泄漏
  • 所以,Java虚拟机采用的是另一种方法来判断对象是否存活,它就是可达性分析算法。

可达性分析算法

Java、C#等主流语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

定义

  • 首先要确定一系列根对象(GC Roots),并从根对象为起点根据对象之间的引用关系搜索出一条引用链(Reference Chain)
  • 在引用链的对象就存活,而不在引用链的对象就认定为可回收对象。

程序员小灰举了个十分生动的例子:

有一个比喻十分恰当:可达性分析算法就好比是在清洗葡萄串,我们可以从一根枝提起一大串葡萄,他们就像一串引用链,而没有和引用链相连的对象就像是散落在池子里的葡萄,可以回收。

根对象(GC Roots)

  • 全局性引用,对方法区的静态对象、常量对象的引用

    1. 虚拟机栈中引用的对象(正在运行的方法使用到的变量、参数等)
    2. 方法区中类静态属性引用的对象(static关键字声明的字段)
    3. 方法区中常量引用的对象,(也就是final关键字声明的字段)
  • 执行上下文,对 Java方法栈帧中的局部对象引用、对 JNI handles 对象引用

    1. 本地方法栈中引用的对象(native方法)
    2. Java虚拟机内部的引用。(系统内部的东西当然能作为根了)
  • 已启动且未停止的 Java 线程

11.2 如何进行垃圾回收:

判断了内存种哪些垃圾需要被回收之后接下来就要通知JVM进行垃圾回收了,在此我们会了解到几个重要的回收算法,”标记-*算法“(可以说十分见名之意了)

下列图源:bugstack虫洞栈——面经#27《JVM 判断对象已死,实践验证GC回收》

标记-清除算法(mark-sweep)

在这里插入图片描述

如图:简单来说,就是用可达性算法判断出哪些垃圾,并就地释放。

要点:

  1. 需要注意的是:所谓的清除,并不需要真正地把整个内存的字节进行清零操作,只需要把空闲对象的起始结束地址记录下来放入空闲列表里,表示这段内存是空闲的就行。

  2. 优缺点:

    1. 优点:速度快,只需要做个标记就能知道哪一块需要被回收,但是他的缺点也是致命的。

    2. 缺点

      1. 一是执行效率不稳定,

      2. 二是会涉及到内存碎片化的问题,如下图,有空间但是不连续导致无法存入,造成内存浪费

        在这里插入图片描述

所谓标记复制算法和标记整理算法,都是对标记清除算法缺点的改进,所以才说标记清除算法是最基础的方式。

标记-整理算法(mark-compact))

在这里插入图片描述

如图:简单来说,用可达性算法找出需要回收的垃圾,回收并释放空间后把空间整理为连续存储的内存空间。

要点:

  • 1974年,Edward Lueders 提出了标记-压缩算法,标记的过程和标记清除算法一样,但在后续对象清理步骤中,先把存活对象都向内存空间一端移动,然后在清理掉其他内存空间。
  • 这种算法能够解决内存碎片化问题,但压缩算法的性能开销也不小。效率就低了。
  • 标记-整理算法 不仅可以弥补 标记-清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价;

标记-复制算法(mark-copy)

在这里插入图片描述

如图:简单来说,用可达性算法找出需要回收的垃圾后,把非垃圾资源连续复制至to空间,实现资源释放。

要点:

  • 这种方式是把内存区域分成两份,分别用两个指针 from 和 to 维护,并且只使用 from 指针指向的内存区域分配内存。
  • 当发生垃圾回收时,则把存活对象复制到 to 指针指向的内存区域,并交换 from 与 to 指针。
  • 它的好处很明显,就是解决内存碎片化问题。但也带来了其他问题,堆空间浪费了一半。

面试题:如何判断哪个是to区呢?一句话:谁空谁是to

12.进行垃圾回收

以上算法并不是单兵作战,而是会在JVM里分代协同回收。

漫画:什么是JVM的垃圾回收?

在这里插入图片描述

上图所示,就是Java堆内存的划分。为什么需要这么划分区域呢?那是因为我们的java对象寿命都是不同的,有的可能需要长时间使用,而有的可能用完就可以丢去。于是我们可以根据其生命周期的不同特点,进行不同的垃圾回收策略。

总的来说,新生代的垃圾回收比较频繁,老年代很久才触发一次垃圾回收。
新生代处理的都是一些朝生夕死的对象,而老年代回收的是更有价值的,会长时间存活的对象。

举个很好理解的例子:新生代处理垃圾,就像是处理生活日用垃圾,而老年代处理的垃圾,更像是过年大扫除,家里实在太多垃圾了来一次重清理。大扫除清理的垃圾,都是在家中存放时间较长的,往往可能曾经很受用,如今退役了先放着过年再打扫清除掉。

大致步骤:

首先等新生代的伊甸园区满了之后用Minor GC进行垃圾回收,筛选一部分资源进入老年代

等老年去空间满了之后,触发Full GC进行垃圾回收

  • 新生代:

    • 年轻代中使用的是Minor GC,采用的就是复制算法(mark-copy)

    在这里插入图片描述

    Minor GC 会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移动到Old generation中,也就是说,一旦收集后,Eden就是变成空的了

    • 当对象在Eden(包括一个Survivor区域,这里假设是From区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳

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

      在这里插入图片描述

    -XX:MaxTenuringThreshold 任期门槛=>设置对象在新生代中存活的次数

    面试题:如何判断哪个是to区呢?一句话:谁空谁是to

  • 老年代:

    • 老年代使用的是Full GC,采用的是清除(sweep)与整理(compact)算法
      • 在整理压缩阶段,不再对标记的对象作回收,而是通过所有存活对象都像一端移动,然后直接清除边界 以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被 清理掉,如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比 维护一个空闲列表显然少了许多开销。
      • 标记、整理算法不仅可以弥补 标记、清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价;

小结:

  • 内存效率:

    • 复制算法 > 标记清除算法 > 标记压缩算法 (时间复杂度)
  • 内存整齐度:

    • 复制算法 = 标记压缩算法 > 标记清除算法
  • 内存利用率:

    • 标记压缩算法 = 标记清除算法 > 复制算法

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

难道就没有一种最优算法吗?猜猜看,下面还有

答案 : 无,没有最好的算法,只有最合适的算法 。

分代收集算法👍

年轻代:(Young Gen)

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

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

老年代:(Tenure Gen)

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

这种情况,存在大量存活率高的对象,复制算法明显变得不合适。
一般是由标记清除或者是标记清除与标记整理的混合实现。Mark阶段的开销与存活对象的数量成正比,这点来说,对于老年代,标记清除或 者标记整理有一些不符,但可以通过多核多线程利用,对并发,并行的形式提标记效率。Sweep阶段的 开销与所管理里区域的大小相关,但Sweep “就地处决” 的 特点,回收的过程没有对象的移动。使其相对其他有对象移动步骤的回收算法,仍然是是效率最好的,但是需要解决内存碎片的问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值