JVM 垃圾回收详解

在 Java 编程中,理解 JVM(Java Virtual Machine)的垃圾回收机制是非常重要的。垃圾回收是 JVM 自动管理内存的关键部分,它确保了程序在运行过程中不会因为内存泄漏而崩溃,同时也提高了开发效率,让开发者无需手动管理内存。本文将详细介绍 JVM 垃圾回收机制。

一、JVM 内存结构概述

在深入了解垃圾回收之前,我们先来了解一下 JVM 的内存结构。JVM 内存主要分为以下几个区域:

  1. 方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 Java 8 及之前版本,方法区也被称为永久代(PermGen);从 Java 8 开始,使用元空间(Metaspace)来替代永久代,元空间使用本地内存,不再受限于 JVM 内存大小。
  2. 堆(Heap) :这是 JVM 管理的最大一块内存区域,用于存储对象实例和数组等。几乎所有的对象实例都在这里分配内存。堆又分为新生代(Young Generation)和老年代(Old Generation)。
    • 新生代:通常用来存放新创建的对象。新生代又分为 Eden 区、Survivor From 区和 Survivor To 区。新创建的对象首先在 Eden 区分配内存,当 Eden 区满了之后,会触发一次 Minor GC(Young GC),将存活的对象复制到 Survivor From 区,然后清空 Eden 区。经过多次 Minor GC 后,仍然存活的对象会被晋升到老年代。
    • 老年代:存放经过多次 Minor GC 后仍然存活的对象。当老年代满了之后,会触发 Major GC(Full GC)。
  3. 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,它是线程私有的。
  4. 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。虚拟机栈也是线程私有的。
  5. 本地方法栈(Native Method Stacks):与虚拟机栈类似,但是它用于执行本地方法(Native Method)。

二、垃圾回收算法

JVM 中主要有以下几种垃圾回收算法:

  1. 标记 - 清除算法(Mark and Sweep)

    • 原理:首先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象。
    • 优点:算法简单,容易理解。
    • 缺点:会产生大量的不连续的内存碎片,可能会导致以后在分配较大对象时无法找到足够的连续内存空间。
  2. 复制算法(Copying)

    • 原理:将内存分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另一块内存上,然后再把已使用过的内存空间一次清理掉。
    • 优点:实现简单,运行高效,不会产生内存碎片。
    • 缺点:将内存缩小为原来的一半,浪费了一半的内存空间。
  3. 标记 - 整理算法(Mark and Compact)

    • 原理:首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,最后直接清理掉端边界以外的内存。
    • 优点:不会产生内存碎片。
    • 缺点:移动存活对象的操作比较耗时,效率相对较低。
  4. 分代收集算法(Generational Collection)

    • 原理:根据对象的存活周期将内存划分为新生代和老年代,针对不同代的特点采用不同的垃圾回收算法。
    • 新生代通常采用复制算法,因为新生代中的对象大多生命周期较短,复制算法可以高效地回收内存,并且不会产生内存碎片。
    • 老年代中的对象通常存活时间较长,采用标记 - 清除算法或标记 - 整理算法。

三、垃圾回收器

JVM 中有多种不同的垃圾回收器,下面介绍几种常见的垃圾回收器:

  1. Serial 收集器

    • 特点:单线程收集器,在进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束。
    • 应用场景:适用于客户端应用等对停顿时间要求不高的场景。
  2. ParNew 收集器

    • 特点:是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其他行为和 Serial 收集器完全一样。
    • 应用场景:与 CMS 收集器配合使用,在 Server 模式下的虚拟机中首选的新生代收集器。
  3. Parallel Scavenge 收集器

    • 特点:采用复制算法,是一个关注吞吐量的收集器。吞吐量 = 用户代码运行时间 / (用户代码运行时间 + 垃圾收集时间)。可以通过参数来调整垃圾收集的时间和频率,以达到更高的吞吐量。
    • 应用场景:适用于在后台运算而不需要太多交互的任务。
  4. Serial Old 收集器

    • 特点:Serial 收集器的老年代版本,单线程收集器,采用标记 - 整理算法。
    • 应用场景:在 Client 模式下与 Parallel Scavenge 收集器搭配使用;在 Server 模式下,作为 CMS 收集器发生失败时的后备预案。
  5. Parallel Old 收集器

    • 特点:Parallel Scavenge 收集器的老年代版本,多线程收集器,采用标记 - 整理算法。
    • 应用场景:注重吞吐量以及 CPU 资源敏感的场合。
  6. CMS(Concurrent Mark Sweep)收集器

    • 特点:以获取最短回收停顿时间为目标的收集器。采用标记 - 清除算法,整个过程分为四个阶段:初始标记、并发标记、重新标记、并发清除。初始标记和重新标记阶段需要暂停所有的工作线程,但是这两个阶段的时间非常短;并发标记和并发清除阶段可以与用户线程一起工作,所以总体上看,CMS 收集器的停顿时间非常短。
    • 缺点
      • 对 CPU 资源敏感:在并发阶段,虽然不会导致用户线程停顿,但是会占用一部分 CPU 资源,导致应用程序变慢。
      • 无法处理浮动垃圾:在并发标记和并发清除阶段,由于用户线程还在继续运行,所以会产生新的垃圾,这些垃圾称为浮动垃圾。CMS 收集器无法在当次收集中处理这些浮动垃圾,只能等到下一次垃圾回收时再进行处理。
      • 会产生内存碎片:由于采用标记 - 清除算法,会产生大量的内存碎片,可能会导致在分配大对象时无法找到足够的连续内存空间。
  7. G1(Garbage-First)收集器

    • 特点:面向服务端应用的垃圾收集器,主要目标是在满足短停顿时间的同时达到高吞吐量。G1 收集器将堆内存划分为多个大小相等的 Region,每个 Region 可以是新生代也可以是老年代。它可以根据不同的 Region 的垃圾堆积情况,优先回收垃圾最多的 Region,从而实现高效的垃圾回收。
    • 过程:G1 收集器的垃圾回收过程主要包括以下几个阶段:初始标记、并发标记、最终标记、筛选回收。初始标记阶段和 CMS 收集器的初始标记阶段类似,需要暂停所有的工作线程,但是时间非常短;并发标记阶段和 CMS 收集器的并发标记阶段类似,可以与用户线程一起工作;最终标记阶段需要暂停所有的工作线程,但是时间也非常短;筛选回收阶段会根据用户设定的停顿时间,选择垃圾最多的 Region 进行回收,在回收过程中也会尽量减少停顿时间。
    • 优点
      • 可预测的停顿时间:G1 收集器可以在不牺牲吞吐量的前提下,尽可能地减少停顿时间,并且可以通过参数设置来指定停顿时间的目标。
      • 并行与并发:G1 收集器在回收垃圾时,可以充分利用多 CPU、多核环境下的硬件优势,使用多个线程来进行垃圾回收,从而提高垃圾回收的效率。同时,在部分阶段可以与用户线程并发执行,进一步减少停顿时间。
      • 分代收集:G1 收集器依然采用分代收集的思想,但是它将整个堆内存划分为多个 Region,每个 Region 可以根据需要扮演新生代或者老年代的角色,这样可以更加灵活地进行垃圾回收。

四、如何优化垃圾回收

  1. 合理设置堆内存大小 :通过调整 -Xms(初始堆大小)和 -Xmx(最大堆大小)参数,可以避免堆内存过小导致频繁的垃圾回收,或者堆内存过大导致长时间的垃圾回收停顿。一般来说,可以根据应用的实际需求和服务器的硬件资源来合理设置堆内存大小。
  2. 选择合适的垃圾回收器:不同的应用场景适合不同的垃圾回收器。例如,如果对停顿时间要求非常高,可以选择 CMS 或 G1 收集器;如果注重吞吐量,可以选择 Parallel Scavenge 和 Parallel Old 收集器。
  3. 避免创建过多的短期对象:如果应用中创建了大量的短期对象,会导致新生代频繁进行垃圾回收,从而影响性能。可以通过对象复用、缓存等方式来减少短期对象的创建。
  4. 监控和分析垃圾回收:可以使用 JVM 提供的工具(如 jstat、jvisualvm 等)来监控垃圾回收的情况,包括垃圾回收的频率、停顿时间、内存使用情况等。通过分析这些数据,可以及时发现问题并进行优化。

五、总结

JVM 的垃圾回收机制是一个复杂而重要的系统,它自动管理内存,为 Java 程序的稳定运行提供了保障。理解垃圾回收的原理、算法和收集器,以及如何优化垃圾回收,对于开发高效、稳定的 Java 应用程序至关重要。通过合理地设置 JVM 参数、选择合适的垃圾回收器以及优化代码,可以有效地提高应用程序的性能和稳定性。

相关推荐
lpruoyu4 小时前
颜群JVM【04】助记符
jvm
Flash Dog4 小时前
【JVM】——实战篇
jvm
DKPT4 小时前
JVM栈溢出和堆溢出哪个先满?
java·开发语言·jvm·笔记·学习
m0_475064504 小时前
jvm双亲委派的含义
java·jvm
胡小禾4 小时前
JDK17和JDK8的 G1
jvm·算法
海梨花4 小时前
今日八股——JVM篇
jvm·后端·面试
fwerfv34534513 小时前
使用PyTorch构建你的第一个神经网络
jvm·数据库·python
Arva .17 小时前
JVM自动内存管理
jvm
Arva .20 小时前
JVM类加载
jvm
小熊出擊20 小时前
【pytest】finalizer 执行顺序:FILO 原则
python·测试工具·单元测试·pytest