从一张图认识Java JVM

gomkiri 发布于 2025-10-30 85 次阅读


AI 摘要

探索JVM内存奥秘:线程私有的程序计数器如何精准定位代码执行位置?堆空间为何要分新生代与老年代?垃圾回收如何通过可达性分析精准标记废弃对象?本文用一张超全架构图,带你穿透JVM核心机制,解密对象从创建到回收的全生命周期。

首先,奉上超牛的 JVM 概览图:

JVM 内存区域

首先我们来看左上角的内存区域,他展示了在 JVM 中都被划分了哪些区域:

  1. 线程私有区域,这部分区域中的数据是每个线程私有的,换句话说:每个线程都有属于自己的程序计数器。
    • 程序计数器:
      记录本线程正在执行 Java 方法的 JVM 指令地址;作为现代语言,Java 程序会并发的形式执行,这就意味着一个线程的执行不是连续的,而是分成了多个时间片,线程计数器的作用就是当一个线程拿到 CPU 使用权限时可以立即知道代码执行到了哪里。
    • 本地方法栈:
      当一个 Java 方法通过 JNI 调用一个本地方法时,JVM 会为其在本地方法栈中创建一个本地栈帧。
    • 虚拟机栈:
      其中是一个个栈帧,每当调用新的方法,都会在栈中添加一个栈帧,那个栈帧又包含了:局部变量表、操作数栈、动态链接和方法出口,可以说栈的主要作用就是管理线程的局部变量和方法调用的上下文
  2. 线程共享区域,整个 JVM 中只有一份,所有线程共享
    • 方法区:
      这是个概念空间,在 JVM 中又分为了元空间和运行时常量池。其中元空间在 jdk1.8 之前被叫做永久代,作为对空间中。
    • 元空间:
      存储了一个 Class 常量池和与类相关各种信息,比如字段、方法、变量、类型以及执行 class 实例的引用和一个执行类加载器的引用。JIT 代码即时编译后的内容也存放在元空间中。
    • 运行时常量池:
      主要是存放编译器生成的各种字面量和符号引用。
    • 堆空间:
      JVM 中最大一片内存空间,其主要目的就是记录真正的对象实体,在形式上又分为新生代和老年代,新生代又分为伊甸园区和幸存者区。

对象创建过程

对应了图中的右上角部分,其中包含了对象创建过程和类加载的过程。

在类加载过程中又涉及到了各种类加载机制和双亲委派机制。这一块会在下面详细说,我们先过一遍这个对象创建过程:

  1. 类加载检查:
    在这个过程中主要在运行时常量池中去寻找该类的符号引用,如果找到了就代表这个类已经经历了加载类的加载、链接、初始化的过程,否则就要通过双亲委派机制完成类的加载。
  2. 分配内存:
    在方法区中就已经记录了加载这个类需要多大的空间,在这个过程中直接分配固定大小的空间即可。
  3. 完成初始化:
    为每个字段按照 Java 中的默认值进行初始化。
  4. 设置对象头等必要信息。
  5. 执行构造方法:
    这里所说的构造方法就是我们在 Java 代码所写的代码(包括 final 等字段值的设置)

垃圾回收

垃圾回收过程又分为了两部分:如果判断对象时候存活和垃圾对象该如何回收。

判断对象存活算法

引用计数法:

引用计数法:
为每个对象都分配一个引用计数器,记录被引用的次数,当次数为0时就代表可以这个对象已经变成垃圾了,没有其他的任何对象使用该对象,但是这种方式有一个很大的问题,那就是不能解决循环依赖问题,这也是 JVM 没有采用这种方式的直接原因。

可达性分析算法:

从 GC Root Set 出发,向下构建引用链,不处于任何引用链的对象就是垃圾对象。这个算法有一个很重要的点就是如何去选择 GC Root , 整体的选择思路就是文档、活跃的对象:

  1. 方法区中的常亮、静态变量;这两者的生命周期都几乎与类的创建和销毁过程是一致的,不会随着实例的创建和销毁发生改变。
  2. 栈中的变量;主要是局部变量表中的数据,包括基本数据类型数据、方法参数、方法中定义的变量。这些变量与堆中的实例紧密相关,可以根据此快速找到堆中参数方法执行的活跃对象。
  3. JVM 内部引用;JVM 的引用通常包括由启动类加载器加载的核心类、活跃线程的线程引用、JNI 引用、同步锁持有对象的引用等等,这些引用所关联的对象都是 Java 程序能够正常执行的基础,一定要确保他们不能被 GC 回收。

垃圾回收算法

使用可达性分析算法完成垃圾的标记之后,就需要在堆空间中堆对垃圾进行清理,JVM 有三种垃圾回收方式:

  1. 标记-清除算法
    将所有的垃圾都找出来后,直接原地清除,不进行另外的移动操作,这种做法的优点是效率高,但是缺点也很明显:很容易造成大量的内存碎片空间,且出现一次垃圾回收后仍然无法为大对象分配内存空间而导致开销更大的 Full GC 的可能性会更大。
  2. 标记-整理算法
    相较于标记清除,不会直接将垃圾回收,而是先把依然存活的对象移动到内存的另一侧,然后再将垃圾给清理掉,这种做法耗时较大但是不会产生内存碎片。
  3. 复制算法
    直接将内存分为两块,每次完成 GC 时都会将存活的对象复制到另一块内存区域中,然后再进行垃圾回收,复制算法便于管理,效率较高,但是最多只能使用一半的内存空间。

通过对比上面三种算法的对比我们会发现,每种算法都会暴露出一些问题,没有一个方法可以作为 GC 的最佳方案,所以 JVM 又引入了分代思想,将整个堆空间分为了新生代和老年代,两者分别设置不同的空间大小并独立完成回收。除此之外,又在新生代中划分了一个伊甸园区和两个幸存者区(这三者的默认空间大小比例是 3:1:1),很显然,这就是为复制算法所设计。同时,每个对象都有一个年龄的概念,新对象年龄为 0 ,并放置在伊甸园区,每经过一次垃圾回收后就将所有存活对象的年龄加一,在年龄达到 15 之后,对象会在两个幸存者区中来回倒腾,直到经过 15 次垃圾回收,对象晋升到老年代。

垃圾回收期器:

在早些版本的垃圾回收器中,通过会使用分代的思想,分别在新生代和老年代中使用不同的垃圾回收器,比如在新生代中,一开始使用的是单线程回收的 Serial,其特点就是实现简单、内存占用少,但是会造成较长的暂停时间,于是又引入了多线程的 ParNew,可以大大减少 STW 的时间。在老年代中,有 Serial 的老年代版本 Serial Old和以最小停顿时间为目标的并发收集器 CMSCMS 将回收阶段分成了 初始标记、没有时停的并发标记、重新标记和并发清除四个阶段,但是 cms 的回收方式是 标记-清除方式,可能会出现大量的内存碎片, 造成内存回收失败,这个时候就会放弃 CMS的处理方式,退回到单线程的 Serial Old

在后期的垃圾回收器中,会弱化分代的思想,以分块思想来代替分代,将堆空间分为多个块,每个块都可以动态的作为伊甸园区、幸存者区和老年代的其中一个。G1 还有一个特点就是:它可以通过参数来指定目标停顿时间。