Java GC 以 G1回收器为例

Java垃圾回收是非常重要的一个知识点,无论是代码开发还是面试过程中都是避不开的问题。

Java作为和C++抗衡的一大重要因素就是内存管理,Java的垃圾回收期能够有效清理堆空间的垃圾,使得程序能够在有限的分配空间下正常运行。通常来说,会和该知识点关联的内容包含但不限于:JVM运行时数据区的结构、垃圾回收算法、JDK1.8前后的JVM结构变化,方法区是什么? JVM与垃圾回收期相关的指令参数是什么? JVM调优实战... 


回顾JVM结构与运行时数据区

涉及垃圾回收期,避不开先要梳理JVM的结构与运行时数据区

JVM的结构:

运行时数据区:


堆空间基本结构

从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)
  2. 老生代(Old Generation)
  3. 永久代(Permanent Generation)

当JDK使用永久代时,由于永久代用于存储类的元数据和字符串常量池等信息,因此不会触发堆内存的垃圾回收。但是,由于永久代的大小有限,当加载的类或字符串常量池等信息过多时,可能会导致永久代内存溢出(OutOfMemoryError),此时会触发永久代回收。一般情况下,可以通过调整永久代的大小以避免内存溢出的问题。

且面试常考的一个问题,什么时候会触发full gc?


在JDK1.7之前,会触发full gc的情况包括:

1)老年代空间不足,无法进行对象晋升;

2)永久代(PermGen)空间不足;

3)System.gc()被显式调用。


JDK1.8及其之后,Full GC 会在 JVM 垃圾回收机制中的“新生代”和“老年代”的空间使用达到一定的比例阈值之后,才会被触发。

根据不同的情况,会有以下几种情况会触发 Full GC:

  1. 内存分配担保失败触发 Full GC:当程序在新生代进行内存分配时,如果新生代内存不足以容纳新对象,就会通过担保机制把新生代内存里的部分对象转移到老年代中,如果担保机制转移后还是不足,就会触发 Full GC。

  2. 显式调用 System.gc() 方法触发 Full GC:由于 System.gc() 只是向 JVM 建议进行垃圾回收,并不能保证立即执行,所以不建议显式调用此方法。

  3. 永久代空间满触发 Full GC:对于使用 JDK1.7 或更早版本的程序,可能会出现由于持续不断的类加载和卸载导致永久代空间不足的情况,此时会触发 Full GC,但是 JDK1.8 已经废弃了永久代,采用元数据空间来实现类存储。

  4. 动态改变堆空间大小时触发 Full GC:如果程序在运行中动态改变堆空间大小,可能会触发 Full GC,因为这个操作会导致新生代和老年代分配的比例达到阈值。

总的来说,Full GC 会造成较长时间的停顿,所以应该尽量避免触发 Full GC,最好通过调整 JVM 参数来优化内存使用效率。

在JDK1.8之前,Java虚拟机的永久代(PermGen)用来存储静态类信息、字符串常量等,而在JDK1.8中,永久代被元空间(Metaspace)所取代。且应注意到,元空间使用的是直接内存。


什么样的对象需要垃圾回收

引用计数法

给对象中添加一个引用计数器:

  • 每当有一个地方引用它,计数器就加 1;
  • 当引用失效,计数器就减 1;
  • 任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

所谓对象之间的相互引用问题,如下面代码所示:除了对象 objAobjB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
    }
}

可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。

哪些对象可以作为 GC Roots 呢?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

对象可以被回收,就代表一定会被回收吗?

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。


常见垃圾回收算法

JVM的垃圾回收机制是遵循分代收集理论进行设计的

Java垃圾回收算法通常分为以下几种:

  • 标记-清除算法
    • 优点:速度快
    • 缺点:内存碎片化

  • 标记-复制算法
    • 优点:内存连续
    • 缺点:内存占用

  • 标记-整理算法
    • 优点:内存连续
    • 缺点:整理效率低

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

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


方法区是什么概念?

方法区也是一块用于存储元数据的区域。它存储的是类的信息、常量池、方法的信息、Field的信息等。

JVM与垃圾回收相关的指令参数是什么?

JVM与垃圾回收期相关的指令参数主要包括:

  • Xms:初始堆大小
  • Xmx:最大堆大小
  • Xmn:年轻代大小
  • XX:SurvivorRatio:年轻代中Eden区域与Survivor区域的比例
  • XX:+UseG1GC:使用G1垃圾回收器

JVM调优实战

在配置Java垃圾回收器时,需要注意以下几点:

  • 避免使用默认的垃圾回收器
  • 根据应用程序的内存需求和性能需求选择合适的垃圾回收器
  • 根据应用程序的性质和负载特征进行调优

主流的垃圾回收器包括:

  • Serial GC
  • Parallel GC
  • CMS GC
  • G1 GC

每种垃圾回收器都有其优缺点,需要根据具体情况进行选择。

每个收集器专攻的区域也是不同的,"?"对应的收集器即为G1收集器。

JVM内存管理:深入垃圾收集器与内存分配策略

JDK1.8的默认垃圾回收器是什么?

这是一道经典面试题,笔者也实际经历过

我们在cmd输入参数即可查看

java -XX:+PrintCommandLineFlags -version

 我们通过控制台输出的信息可以看到,当前使用的是UserParallelGC

这表明当前用的收集器为Parallel Scavenge + Parallel Old收集器,我们可以进一步打印详细信息,输入指令

java -XX:+PrintGCDetails -version

输出结果为

java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
Heap
 PSYoungGen      total 152576K, used 5243K [0x0000000716000000, 0x0000000720a00000, 0x00000007c0000000)
  eden space 131072K, 4% used [0x0000000716000000,0x000000071651ed08,0x000000071e000000)
  from space 21504K, 0% used [0x000000071f500000,0x000000071f500000,0x0000000720a00000)
  to   space 21504K, 0% used [0x000000071e000000,0x000000071e000000,0x000000071f500000)
 ParOldGen       total 348160K, used 0K [0x00000005c2000000, 0x00000005d7400000, 0x0000000716000000)
  object space 348160K, 0% used [0x00000005c2000000,0x00000005c2000000,0x00000005d7400000)
 Metaspace       used 2353K, capacity 4480K, committed 4480K, reserved 1056768K
  class space    used 254K, capacity 384K, committed 384K, reserved 1048576K

可以看到有Heap和Metaspace的空间分配情况

Parallel Scavenge收集器

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用标记-复制算法,老年代采用标记-整理算法。

Parallel Old收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

G1回收器

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

和其他垃圾回收器相比,G1垃圾回收器的堆空间被划分为多个相同大小的区域,每个区域可能是Eden、Survivor或Old区。这样,G1回收器可以并发地执行垃圾回收,从而大大减少了停顿时间。

对应的内存布局,内存划分为大小相同的独立region,根据需要动态划分为不同的区:

每个区的角色,根据运行的需要相互转换。而白色的region是代表未利用的区域。

G1算法中对大对象的判定:

一个对象,超过一个region空间大小的1/2时则被认为是大对象,他们会直接创建再H区域。

可以在用户容忍范围内进行操作


G1回收器的主要优势包括:

  • 并行回收
  • 可预测的停顿时间
  • 堆空间的整理

然而,G1回收器也存在一些缺点:

  • 对CPU的占用较高
  • 垃圾回收期的可预测性不如CMS GC

G1垃圾回收器的工作流程如下:

  1. 初始标记:标记所有从根对象直接可达的对象。这个阶段需要暂停应用程序。
  2. 并发标记:并发地标记全部对象。
  3. 最终标记:标记所有从根对象可达的对象。这个阶段需要暂停应用程序。
  4. 筛选回收:计算每个区域中存活对象的成本,并决定在哪个区域执行垃圾回收。

G1垃圾回收器关注的指标包括:

  • 最大停顿时间:垃圾回收器需要尽可能减少应用程序的停顿时间。
  • 垃圾回收频率:垃圾回收器需要尽可能减少垃圾回收的频率。
  • 堆空间利用率:垃圾回收器需要尽可能高效地利用堆空间。
  • 并行性:垃圾回收器需要尽可能利用多核处理器的并行能力。

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。


参考资料:

What is JVM (Java Virtual Machine): Architecture Explained! (guru99.com)

Java 内存区域详解(重点) | JavaGuide(Java面试+学习指南)

14 动画+源码:演示G1垃圾回收算法的内存布局_哔哩哔哩_bilibili

面试题之JDK8默认垃圾回收器是什么_另一花生的博客-优快云博客

15 动画讲解:G1垃圾回收算法的年轻代垃圾回收,为什么比过去的垃圾回收器快_哔哩哔哩_bilibili 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值