JVM 内存和垃圾回收
约 2267 字大约 8 分钟
2026-05-23
JVM 内存区域划分
总览图

运行时数据区域:在 JVM 内的内存
直接内存:JVM 之外的内存空间。
线程共享-堆内存
这里几乎存放所有的对象实例和数组,是最大的一块内存,也是垃圾回收的主要区域。
这里处理垃圾回收很多,为了便于管理,通常会有分代处理,分为新生代和老年代。
新生代会继续划分为
Eden区和Survivor区(包含From Survivor和To Survivor,比例为 1:1),二者比例为 8:2老年代存放生命周期较长的对象。
提示
堆内存满了,就会抛出 OutOfMemoryError 错误。
线程共享-方法区(元空间)
方法区,这里存的是虚拟机加载好的类型信息、常量、静态变量、即时编译后的代码缓存等,包含了一个 运行时常量池。
方法区和元空间的概念:方法区是逻辑上的概念,元空间是具体的实现方法。
这里没地方了也会抛出错误:OutOfMemoryError
线程私有-程序计数器
可视为:字节码行号指示器。
生命周期和线程相同,执行本地方法时计数器值为空。
CPU 会不断的切换,它就是线程继续执行时的导航。
线程私有-本地方法栈
所谓的本地方法,其实就是 非 Java 语言 编写的方法,通过 JNI 来调用,一般是操作系统、硬件啊等 Java 无法直接完成的操作,这些方法是 JVM 以外的代码提供的。
为什么线程私有?因为每个线程调用的本地方法是不一样的~
线程私有-虚拟机栈
这里先说名字,这个奇怪的名字是为了强调,这是 JVM 模拟的,而非操作系统原生,所以叫虚拟机+栈,栈是它的数据结构。每个线程执行的方法可能都不一样,所以这里是私有的。
既然它是个栈,就有栈帧,每个方法执行,就创建一个栈帧,入栈,执行完了再出栈。所以,很明显,栈帧存储的就是方法相关的信息。
栈帧
栈帧有四类信息:局部变量表、操作数栈、动态链接、方法返回地址。
局部变量表:保存了方法内的参数、定义的局部变量。
操作数栈:其实就是临时空间,存储方法计算的中间值。(又是一个奇怪的名字……但是线程不会一直占有 CPU,有临时空间就理所当然啦,否则下一次来的话,总不能从头开始运行吧。)
动态链接:Java 代码在编译为字节码时,方法调用一般不会写固定的内存地址,而是写一个 符号引用 ,比如多态,只能在类加载运行时才知道具体类,这些符号引用存储在 运行时常量池 中,用到的时候再去找,Java 的 多态 、反射 底层就是这样来的。
方法返回地址:这个就字面意思,方法执行完了回到哪里去,抛异常了又去哪里?就是这个地址。
这里还需要注意一点,有 2 个错误:
StuckOverflowError: 栈深超过虚拟机允许的深度OutofMemoryError: 没内存了。
垃圾回收-找垃圾
引用计数法
原理:引用时 +1,不引用了 -1,计数器为 0 时回收。
问题:无法决绝循环引用问题,一般不用这个。
可达性分析法
原理:从一组 GC Roots 的对象出发,通过引用关系向下搜索,形成引用链条,如果没有引用链则回收。
引用链会区分 4 种类型,会影响回收的时机。
- 强引用:类似
new Object(),此类引用,JVM 不回回收对象。 - 软引用:发生在 OOM 之前,会将这些对象进行二次回收。
- 弱引用:对象会生存到下一次 GC 前。
- 虚引用:无法获取实例,用于对象回收时收到系统通知。
提示
GC Roots 由很多部分组成,例如虚拟机栈内的对象、方法区内的对象等等。
垃圾回收-回收垃圾
标记-清除法
字面意思,分为标记和清除两步。
- 标记:从
GC Roots出发,标记所有可达对象。 - 清除:遍历所有对象,回收未被标记的对象。
存在很明显的问题:效率不高,清除后由很多不连续的内存碎片。大对象可能无法分配内存而再次触发 GC。
复制算法
将内存分为大小相等的两块,每次使用其中一块,用完后,将存活的对象复制到另一块中,然后一次性清理掉一半的内存。
非常简单高效,没有碎片问题。
提示
还记得堆内存中的新生代 Eden:Survivor = 8:1:1 吗?这里的 Survivor 1:1 就是复制算法啦。
标记-整理法
标记和整理两步。
标记:和上面的「标记-清除法」一样。
整理:将所有的存活对象移动到内存的一端,边界之外的内存直接清除掉。
也很明显,效率不高。
分代收集法
依据上述理论,堆内存分为新生代和老年代,分代收集法,其实是不同的「代」采用不同的收集算法。
- 新生代:每次有大量的对象消亡,用高效的复制算法。
- 老年代:这里的对象有很长的生命周期,存活率非常高,用「标记-清除」或者「标记-整理」
垃圾回收器简述
说完了「找垃圾」、「回收垃圾」的方法之后,就该落实到实际的内容啦,垃圾回收器,就是找垃圾、回收垃圾的机器,这种机器有很多,看重点的几个就好。
G1 (Garbage First) - 适用广
简介:G1 是面向服务器的垃圾收集器,针对多处理器大内存的服务器,高概率满足GC停顿时间和高吞吐。平衡了吞吐量和延迟,适合大内存(4G+) 的场景,适合大多数企业级应用场景。
设计目标:延迟可控条件下,尽可能的满足吞吐量。
核心思想:堆内存划分为多个相等的区域(Region),但是仍会扮演新生代、老年代的角色。
运作步骤:
- 初始标记:短暂停顿(STW, Stop The World),标记从
GC Roots直接可达的对象。 - 并发标记:和应用一同运行标记可达对象,可能持续较长时间,取决于堆内存大小。
- 最终标记:短暂停顿标记少量未处理的对象
- 筛选回收:对多个 Region 进行价值排序,优先回收垃圾最多的,复制存活对象到新区域,可能会触发多次短暂停顿法。
整体是「标记-整理法」,局部包含「复制算法」
JDK 9 以后是默认的垃圾回收器。
ZGC - 增长快
简介:ZGC(Z Garbage Collector) 专门为超低延迟和超大内存设计的,低延迟首选。适合金融、实时游戏服务器等场景。
设计目标:
- 低延迟:直击传统 GC 痛点,将 STW 降低到 10ms 内,JDK 21 可以达到 1ms 内。
- 可扩展:STW 时间不随 堆内存大小、存活对象数量变化。
- 大内存:8mb 到 16TB
- 吞吐量可控:损失不超过 15%
核心思想:
- 染色指针:将 GC 状态直接写到对象引用地址内。可直接读取对象状态,对象移动、压缩时也直接修改指针位,不需要修改指向它的指针地址
- 读屏障:线程从堆内存中读取对象时,ZGC 会通过读屏障出触发代码的轻量级检查,直接处理引用的对象,可在线程无感知的情况继续运行,保障并发压缩可能。
运作步骤:
- 初始标记,也就是 STW.
- 并发标记,同 G1.
- 再标记,处理并发标记期间发生变化的对象。
- 初始转移:选择需要压缩和转移的内存区域。
- 并发转移:和应用一同运行,将存活对象转移到新的内存区域,耗时较长。若线程需要读取正在移动的对象,读屏障会处理。
JDK 11 引入,JDK 15 成为默认 GC,JDK 21 引入分代 ZGC,JDK 23 分代模式成为默认 GC。
提示
当然,这是核心思想和技术,它还用了很多其他的技术,暂时不深入了解啦。
启用它需要加参数(下面的内容适合 JDK 23 以下,JDK 23 以后是默认的)
java -XX:+UseZGC classNamejava -XX:+UseZGC -XX:+ZGenerational版权所有
版权归属:FelixJY
