美文网首页Android开发
GC 回收机制与分代回收策略

GC 回收机制与分代回收策略

作者: Android进阶架构 | 来源:发表于2020-11-08 22:24 被阅读0次

    GC 回收机制与分代回收策略

    垃圾回收(Garbage Collection,简写为 GC)可能是虚拟机众多知识点中最为大众所熟知的一个了,也是Java开发者最关注的一块知识点。Java 语言开发者比 C 语言开发者幸福的地方就在于,我们不需要手动释放对象的内存,JVM 中的垃圾回收器(Garbage Collector)会为我们自动回收。但是这种幸福是有代价的:一旦这种自动化机制出错,我们又不得不去深入理解 GC 回收机制,甚至需要对这些“自动化”的技术实施必要的监控和调节。

    什么是垃圾

    所谓垃圾就是内存中已经没有用的对象。 既然是”垃圾回收",那就必须知道哪些对象是垃圾。Java 虚拟机中使用一种叫作可达性分析的算法来决定对象是否可以被回收。

    可达性分析

    JVM 把内存中所有的对象之间的引用关系看作一张图,通过一组名为”GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:

    注意:上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。

    GC Root 对象

    在 Java 中,有以下几种对象可以作为 GC Root

    1. Java 虚拟机栈(局部变量表)中的引用的对象。
    2. 方法区中静态引用指向的对象。
    3. 仍处于存活状态中的线程对象。
    4. Native 方法中 JNI 引用的对象。

    注意:全局变量同静态变量不同,它不会被当作 GC Root。

    什么时候回收

    不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。

    1. Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
    2. System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。

    如何回收垃圾

    由于垃圾收集算法的实现涉及大量的程序细节,各家虚拟机厂商对其实现细节各不相同,因此并不会过多的讨论算法的实现,只是介绍几种算法的思想以及优缺点。

    标记清除算法(Mark and Sweep GC)

    从”GC Roots”集合开始,将内存整个遍历一次,保留所有可以被 GC Roots 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分两步。

    1. Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
    2. Sweep 清除阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。
    标记清除算法
    • 优点:实现简单,不需要将对象进行移动。
    • 缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。

    复制算法(Copying)

    将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

    1. 复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图所示:
    1. 标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图所示:
    • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
    • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

    标记-压缩算法 (Mark-Compact)

    需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:

    1. Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
    2. Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。
    • 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
    • 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。

    JVM分代回收策略

    Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是 JVM 的内存分代策略。注意: 在 HotSpot 中除了新生代和老年代,还有永久代。

    新生代

    新生成的对象优先存放在新生代中,新生代对象存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法。 新生代又可以继续细分为 3 部分:EdenSurvivor0(简称 S0)Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。

    • 绝大多数刚刚被创建的对象会存放在 Eden 区。
    • Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden 区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1 是空的。
    • 下一次 Eden 区满时,再执行一次垃圾回收。此次会将 EdenS0 区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0 变为空。
    • 如此反复在 S0S1 之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。

    老年代

    一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

    我们可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法

    注意:对于老年代可能存在这么一种情况,老年代中的对象有时候会引用到新生代对象。这时如果要执行新生代 GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。

    GC Log 分析

    新生代和老年代所打印的日志是有区别的:

    • 新生代 GC:这一区域的 GC 叫作 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
    • 老年代 GC:发生在这一区域的 GC 也叫作 Major GC 或者 Full GC。当出现了 Major GC,经常会伴随至少一次的 Minor GC。

    注意:在有些虚拟机实现中,Major GCFull GC 还是有一些区别的。Major GC 只是代表回收老年代的内存,而 Full GC 则代表回收整个堆中的内存,也就是新生代 + 老年代。


    引用

    根据引用强度的由强到弱,他们分别是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)

    需要注意的是,被软引用对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,这些创建的软引用并不会自动被垃圾回收器回收掉。

    Android官方在对 SoftReference 的介绍中,也已经不建议使用它来实现缓存功能。

    总结

    虚拟机垃圾回收机制很多时候都是影响系统性能、并发能力的主要因素之一。尤其是对于从事 Android 开发的工程师来说,有时候垃圾回收会很大程度上影响 UI 线程,并造成界面卡顿现象。因此理解垃圾回收机制并学会分析 GC Log 也是一项必不可少的技能。

    Android 虚拟机中对垃圾回收所做的优化。

    喜欢本文的话,不妨顺手给我点个小赞、评论区留言或者转发支持一下呗😜😜😜~
    点击【GitHub】还有彩蛋哦!!!

    相关文章

      网友评论

        本文标题:GC 回收机制与分代回收策略

        本文链接:https://www.haomeiwen.com/subject/btrkbktx.html