JVM 底层彻底理解

JVM 底层彻底理解

  • [一、JVM 到底解决了什么问题(先理解本质)](#一、JVM 到底解决了什么问题(先理解本质))
    • [JVM 的本质目标只有三点](#JVM 的本质目标只有三点)
    • 核心结论
  • [二、JVM 整体架构(为什么要分这些模块)](#二、JVM 整体架构(为什么要分这些模块))
    • [JVM 为什么要分这三层?](#JVM 为什么要分这三层?)
    • [JVM 为什么一定要"拆模块"?](#JVM 为什么一定要“拆模块”?)
  • [三、运行时数据区(JVM 内存模型)](#三、运行时数据区(JVM 内存模型))
    • [JVM 内存只分两大类](#JVM 内存只分两大类)
      • [1. 程序计数器(最小,但不可或缺)](#1. 程序计数器(最小,但不可或缺))
      • [2. 虚拟机栈(排错最常见)](#2. 虚拟机栈(排错最常见))
      • [3. 本地方法栈(补充点)](#3. 本地方法栈(补充点))
      • [4. 堆(GC 的主战场)](#4. 堆(GC 的主战场))
      • [5. 方法区 / 元空间(类的存储区)](#5. 方法区 / 元空间(类的存储区))
    • 总结
  • 四、对象从创建到回收的完整生命周期
    • [1. 对象创建全过程](#1. 对象创建全过程)
    • [2. 内存分配方式(为什么有两种)](#2. 内存分配方式(为什么有两种))
    • [3. 对象创建为什么是线程安全的?](#3. 对象创建为什么是线程安全的?)
  • [五、垃圾回收机制(GC 底层原理)](#五、垃圾回收机制(GC 底层原理))
    • [1. 如何判断对象已死?](#1. 如何判断对象已死?)
    • [2. GC 算法为什么要"组合使用"?](#2. GC 算法为什么要“组合使用”?)
    • [3. G1 垃圾回收器(主流)](#3. G1 垃圾回收器(主流))
    • 总结
  • 六、内存泄漏
    • [1. 内存泄漏的本质定义](#1. 内存泄漏的本质定义)
    • [2. 常见泄漏场景的底层原因](#2. 常见泄漏场景的底层原因)
    • [3. 内存泄漏与 OOM 的关系](#3. 内存泄漏与 OOM 的关系)
  • [七、逃逸分析(JIT 的核心优化能力)](#七、逃逸分析(JIT 的核心优化能力))
    • [1. 什么是逃逸?](#1. 什么是逃逸?)
    • [2. JVM 为什么关心"逃逸"?](#2. JVM 为什么关心“逃逸”?)
    • [3. 三大核心优化](#3. 三大核心优化)
      • [1. 栈上分配](#1. 栈上分配)
      • [2. 标量替换(优化力度最大)](#2. 标量替换(优化力度最大))
      • [3. 锁消除](#3. 锁消除)
    • 总结
  • [八、JVM 调优与问题排查](#八、JVM 调优与问题排查)
    • [1. 常见 OOM 类型(先会分类)](#1. 常见 OOM 类型(先会分类))
    • [2. 排查 OOM 的标准流程](#2. 排查 OOM 的标准流程)
    • [3. 常用 JVM 排查工具](#3. 常用 JVM 排查工具)
    • 总结
  • 九、总结

一、JVM 到底解决了什么问题(先理解本质)

在 JVM 出现之前:

  • C / C++ 需要手动管理内存

  • 内存泄漏、野指针问题频发且难排查

  • 程序强依赖操作系统,跨平台成本高

这些问题的共同点只有一个:

复杂、易错、不可控。


JVM 的本质目标只有三点

一句话先记住:隔离、托管、优化。

  1. 屏蔽底层操作系统差异

    程序不再直接依赖操作系统,而是运行在 JVM 之上,实现"一次编写,到处运行"。

  2. 自动内存管理(GC)

    对象创建、回收由 JVM 统一管理,开发者只关心"是否还在使用",不再和内存细节纠缠。

  3. 通过运行期优化获得接近原生的性能

    JVM 会在运行过程中识别热点代码并进行编译优化,长期运行下性能接近甚至等同原生程序。


核心结论

JVM 不是为了"极限快",而是为了稳定、可控、可长期运行

它本质上是一个:

以空间换安全、以运行期分析换性能

的工程系统。


二、JVM 整体架构(为什么要分这些模块)

JVM 不是一个黑盒,而是一个高度分层、职责清晰的系统:

text 复制代码
ClassLoader  →  Runtime Data Area  →  Execution Engine

一句话理解这条链路:

先把类弄进来 → 给数据找地方 → 把代码跑起来


JVM 为什么要分这三层?

因为 JVM 要同时解决三类完全不同的问题:
加载、存储、执行。


  1. 类加载器(ClassLoader)

    核心职责:把 class 文件变成 JVM 能用的 Class 对象

    • 负责类的加载、校验、链接、初始化
    • 支持按需加载,而不是一次性全部加载
    • 通过双亲委派机制保证核心类安全

    一句话记忆:

    类加载器决定"类从哪来、能不能用"

    高频点:

    1. 为什么需要双亲委派?

      一句话答案:

      防止核心类被篡改,保证类加载的安全性和一致性。

      关键点:

      • 优先由父加载器加载,避免自定义类覆盖 java.lang.*
      • 保证同一个类在 JVM 中的唯一性

    2. 自定义 ClassLoader 的典型场景?

      一句话答案:

      隔离、扩展、动态加载。

      典型场景:

      • 插件化系统(不同插件使用不同依赖)
      • 热加载 / 热部署
      • 应用隔离(如 Tomcat、多应用容器)

  2. 运行时数据区(Runtime Data Area)

    核心职责:给程序运行中的数据分配内存

    • 方法区:类元数据、常量、静态变量
    • 堆:对象实例
    • 虚拟机栈 / 本地方法栈:方法调用、局部变量
    • 程序计数器:线程执行位置

    一句话记忆:

    运行时数据区决定"数据放哪、生命周期多长"

    高频点:

    1. 堆和栈的区别?

      一句话答案:

      堆存对象,栈存方法调用。

      关键点:

      • 堆:线程共享,GC 管理,对象实例
      • 栈:线程私有,方法栈帧,自动回收

    2. 哪些区域线程私有,哪些线程共享?

      一句话答案:

      栈、程序计数器是私有的;堆和方法区是共享的。

      记忆口诀:

      "对象共享,调用私有"


    3. OOM 和 StackOverflowError 的根源?

      一句话答案:

      OOM 是内存不够,SOF 是调用太深。

      对应关系:

      • OOM:堆、方法区内存耗尽
      • StackOverflowError:递归或方法调用层级过深

  1. 执行引擎(Execution Engine)

    核心职责:把字节码真正跑起来

    • 解释执行:启动快
    • JIT 编译:热点代码转为机器码
    • 运行期优化:内联、逃逸分析、锁消除

    一句话记忆:

    执行引擎决定"怎么跑、跑得快不快"

    高频点:

    1. 解释执行 vs JIT?

      一句话答案:

      解释执行启动快,JIT 执行快。

      关键点:

      • 解释执行:逐条解释字节码
      • JIT:热点代码编译为机器码

    2. 什么是热点代码?

      一句话答案:

      被频繁执行、值得编译优化的代码。

      关键点:

      • JVM 通过计数器识别
      • 热点才会触发 JIT

    3. 为什么 Java 能越跑越快?

      一句话答案:

      因为 JVM 会对热点代码持续做运行期优化。

      核心逻辑:

      • 先解释执行
      • 再 JIT 编译
      • 最终稳定在高性能状态

JVM 为什么一定要"拆模块"?

如果不拆,会出现三个致命问题:

  1. 类加载和内存强耦合,无法支持热加载
  2. 内存和执行强耦合,无法做运行期优化
  3. 执行逻辑固化,JIT、GC 无法演进

而拆分之后,JVM 获得了三种能力:

  • 解耦:加载、存储、执行互不干扰
  • 可扩展:ClassLoader、GC、JIT 都可替换
  • 运行期优化:边跑边分析,动态决策

一句话总结

ClassLoader 负责"引入"
Runtime Data Area 负责"承载"
Execution Engine 负责"执行"

JVM 的所有高级特性(GC、JIT、热加载),
都建立在这三层解耦之上。


三、运行时数据区(JVM 内存模型)

JVM 的运行时数据区不是随便划的,它直接决定了:

  • GC 怎么做

  • 多线程是否安全

  • OOM 和 StackOverflowError 从哪来


JVM 内存只分两大类

一句话先记住:

  • 线程私有:只和当前线程有关

  • 线程共享:所有线程共同使用

这是理解 GC、并发安全、内存异常 的根本前提。


1. 程序计数器(最小,但不可或缺)

本质

  • 记录当前线程正在执行的字节码位置
  • 线程切换后能从正确位置继续执行

关键特点

  • 线程私有
  • 唯一不会发生 OOM 的内存区域

高频速答

  1. 为什么 Java 多线程切换不会乱?

    一句话答案:

    每个线程都有独立的程序计数器,保存自己的执行位置。


2. 虚拟机栈(排错最常见)

栈的基本单位:栈帧

每一次方法调用都会创建一个栈帧,包含:

  • 局部变量表

  • 操作数栈

  • 动态链接

  • 方法返回地址

局部变量表里有什么?

  • 基本数据类型
  • 对象引用(reference)

关键认知点:

对象在堆中,栈中只保存引用


两类典型异常(必须能区分)

  1. StackOverflowError

    • 方法递归过深

    • 栈帧不断入栈,空间耗尽

  2. OutOfMemoryError(与栈相关)

    • 线程创建过多

    • 每个线程都要分配栈空间


高频速答

  1. 栈为什么是线程私有的?

    一句话答案:

    方法调用链不能被多个线程共享,否则执行状态会混乱。


3. 本地方法栈(补充点)

  • native 方法服务

  • HotSpot 中通常与虚拟机栈合并实现

    主要存储 native 方法执行过程中使用的:

    • 参数
    • 局部变量
    • 返回值
    • JNI 调用相关的上下文信息
    • 底层平台的调用状态。

一般不单独深挖,但要知道它属于线程私有

native 方法是指:方法的实现不在 Java 代码中,而是由 Java 以外的语言(通常是 C / C++)实现的方法。


4. 堆(GC 的主战场)

堆的核心职责

  • 存放对象实例

  • 线程共享

  • GC 管理的主要区域


为什么堆要"分代"?

不是拍脑袋设计,而是基于一个长期统计结论:

绝大多数对象,存活时间非常短

因此 JVM 才采用分代模型来降低 GC 成本。


新生代结构与流转

新生代分为:

  • Eden
  • Survivor From
  • Survivor To

对象流转路径:

text 复制代码
Eden → S0 → S1 → Old

每次 Minor GC:

  • 存活对象复制到 Survivor

  • 年龄加 1

  • 达到阈值后晋升老年代


老年代的特点

  • 对象存活时间长

  • GC 次数少,但代价高

  • Full GC 的主要区域


高频速答

  1. 为什么 Minor GC 快,Full GC 慢?

    一句话答案:

    新生代对象少且易回收,老年代对象多且存活时间长。


5. 方法区 / 元空间(类的存储区)

JDK 8 的关键变化

  • 移除永久代(PermGen)

  • 引入 Metaspace(元空间)

为什么要这样改?

永久代的问题:

  • 空间固定

  • 容易 OOM

  • 与堆强耦合

元空间的优势:

  • 使用本地内存

  • 可动态扩展

  • 更适合大量类加载(如 Spring、动态代理)


高频点

  1. 为什么永久代被移除?

    一句话答案:

    避免固定大小限制,提升类加载场景下的稳定性。


总结

计数器保执行,切线程不乱
栈管调用,堆管对象
新生代快收,老年代慢清
类信息不进堆,进元空间

这套内存模型,直接支撑了:

  • GC 设计
  • 并发模型
  • JVM 性能优化

四、对象从创建到回收的完整生命周期

常态:
"你 new 一个对象,JVM 到底干了什么?"


1. 对象创建全过程

标准五步,一步不能少:

  1. 类是否已加载

    没加载先触发类加载(加载、链接、初始化)

  2. 分配内存

    在堆中为对象分配空间

  3. 初始化零值

    实例字段设置默认值(保证对象可用)

  4. 设置对象头

    包含类指针、GC 信息、锁信息等

  5. 执行构造方法

    按代码逻辑完成初始化


高配点

  1. 对象创建的完整过程?

    一句话答案:

    检查类 → 分内存 → 清零 → 设对象头 → 调构造方法


2. 内存分配方式(为什么有两种)

对象分配的前提是:

堆内存是否连续。


  1. 指针碰撞

    适用场景:

    • 堆内存连续
    • 如 Serial、ParNew 收集器

    做法:

    • 维护一个指针
    • 分配时指针向前移动

    特点:

    • 实现简单
    • 分配速度快

  2. 空闲列表

    适用场景:

    • 堆内存不连续
    • 如 CMS、G1

    做法:

    • 维护可用内存块列表
    • 从合适位置分配

    特点:

    • 灵活
    • 维护成本更高

高频点

  1. 指针碰撞和空闲列表的区别?

    一句话答案:

    是否要求堆内存连续。


3. 对象创建为什么是线程安全的?

因为对象创建发生在多线程并发环境下。

JVM 主要提供两种方案:


  • 方案一:CAS + 重试

    • 通过原子操作更新内存指针
    • 失败则重试

    优点:通用

    缺点:高并发下有性能损耗


  • 方案二:TLAB(线程本地分配缓冲区)(默认)

    Thread Local Allocation Buffer

    核心思想:

    • 每个线程在堆中预分配一小块内存
    • 线程内对象分配无需加锁

    特点:

    • 无锁
    • 分配速度快
    • 大多数对象直接在 TLAB 中完成分配

高频点

  1. JVM 默认是否开启 TLAB?

    一句话答案:

    是的,默认开启,用于提升对象分配效率。


五、垃圾回收机制(GC 底层原理)

GC 的核心只解决三件事:

对象死没死 → 怎么回收 → 何时回收得更优


1. 如何判断对象已死?

为什么不用引用计数?

引用计数的问题只有一句话:

循环引用无法回收

两个对象互相引用,但外部已不可达,

引用计数不为 0,却早该被回收。


JVM 的标准答案:可达性分析

核心思想:

从一组"必然存活"的对象出发,看能不能走到目标对象。

这组起点,叫 GC Roots


常见 GC Roots

  • 虚拟机栈中的引用
  • 类静态变量
  • 常量池中的引用
  • JNI(本地方法)引用

结论规则:

从 GC Roots 不可达的对象,才是可回收对象


高频点

  1. JVM 如何判断对象是否可回收?

    一句话答案:

    通过 GC Roots 做可达性分析,不可达即回收。


2. GC 算法为什么要"组合使用"?

因为不同区域的对象特性完全不同。


  • 新生代:复制算法

    特点:

    • 对象存活率低

    • 回收频繁

    优势:

    • 只复制存活对象
    • 回收速度快

    代价:

    • 需要额外空间(Survivor)

  • 老年代:标记-整理算法

    特点:

    • 对象存活率高
    • 不能频繁移动

    优势:

    • 避免内存碎片
    • 适合长期存活对象

    代价:

    • 回收成本高

核心结论

没有万能 GC 算法,只有场景最优组合。


高频点

  1. 为什么新生代和老年代用不同算法?

    一句话答案:

    因为对象存活率不同,回收策略必须不同。


3. G1 垃圾回收器(主流)

G1 的出现,本质是为了解决一个问题:

在大堆内存下,如何控制 GC 停顿时间?


G1 的核心设计思想

  • 把整个堆拆成多个 Region
  • Region 可以是新生代,也可以是老年代
  • 不再固定按代整体回收

一句话理解:

不按"代"收,按"价值"收


什么是"回收价值"?

综合考虑:

  • Region 中垃圾比例

  • 回收收益

  • 回收成本

优先回收 性价比最高 的 Region。


为什么 G1 停顿时间可控?

核心原因只有两点:

  • 每次只回收部分 Region

  • 可以根据目标停顿时间做回收计划

这也是 G1 名字的来源:

Garbage First(优先回收垃圾最多的区域)。


高频点

  1. G1 为什么能控制停顿时间?

    一句话答案:

    通过 Region 化和按回收价值选择回收范围。


总结

  1. 死没死看 Roots

  2. 算法选型看存活率

  3. G1 不按代,按价值收


六、内存泄漏

1. 内存泄漏的本质定义

一句话定义:

对象已经没有业务意义,但仍然被 GC Roots 间接或直接引用。

关键点要说清楚:

  • 不是"内存不够"

  • 而是引用关系断不开

  • GC 按规则工作,但规则失效于设计错误


高频点

  1. 什么是内存泄漏?

    一句话答案:

    对象该死但没死,因为还在 GC Roots 引用链上。


2. 常见泄漏场景的底层原因

  1. 静态引用(最基础)

    为什么会泄漏?

    • 静态变量生命周期 = JVM 生命周期
    • 天然属于 GC Roots

    一旦静态集合、静态缓存不断增长:

    对象永远不可回收


    高频点

    1. 为什么静态变量容易导致内存泄漏?

      一句话答案:

      因为它们是 GC Roots,生命周期过长。


  1. ThreadLocal 泄漏

    问题不在 ThreadLocal,而在线程池。

    底层结构:

    • key:ThreadLocal(弱引用)
    • value:业务对象(强引用)

    当:

    • ThreadLocal 被回收
    • 线程长期存活(线程池)

    结果:

    value 仍被线程引用,无法回收


    正确姿势(必须说)

    • 使用后 finally 中调用 remove()

    高频点

    1. ThreadLocal 为什么会内存泄漏?

      一句话答案:

      线程不结束,value 强引用还在。


  1. 连接、流未关闭

    典型对象:

    • JDBC 连接
    • Socket
    • IO 流

    问题本质:

    • 不仅占 JVM 堆
    • 还占用 操作系统资源

    这种泄漏,往往比 OOM 更危险


    高频点

    1. 为什么流不关闭问题严重?

      一句话答案:

      既泄漏内存,又泄漏系统资源。


3. 内存泄漏与 OOM 的关系

一句话因果关系:

内存泄漏是原因,OOM 是最终结果。

  • 少量泄漏 → 系统还能撑
  • 长期泄漏 → 必然 OOM

高频点

OOM 往往不是"突然发生",
而是泄漏长期积累的必然结果。


七、逃逸分析(JIT 的核心优化能力)

让 JVM 判断"对象需不需要进堆"。


1. 什么是逃逸?

标准定义:对象是否可能被方法之外访问。


不逃逸的典型特征

  • 只在方法内部使用

  • 不作为返回值

  • 不赋值给外部变量

一句话记忆:

出不了方法的对象,就是不逃逸。


高频点

  1. 什么是逃逸分析?

    一句话答案:

    JVM 判断对象作用域是否超出方法范围。


2. JVM 为什么关心"逃逸"?

因为一旦确认 不逃逸

对象就没必要进堆。

直接收益:

  • 减少堆内存分配

  • 减少 GC 压力

  • 提升整体性能


高频点

  1. 逃逸分析的核心目的是什么?

    一句话答案:

    减少对象进入堆,从而减少 GC。


3. 三大核心优化

1. 栈上分配

结论型描述:

  • 对象直接分配在栈上
  • 方法结束自动销毁
  • 不参与 GC

适用前提:

  • 对象不逃逸

高频点

  1. 什么是栈上分配?

    一句话答案:

    不逃逸对象直接在栈上分配。

    JVM 在逃逸分析后,不再创建对象, 而是把对象字段拆成标量 ,可能分布在栈帧、操作数栈甚至寄存器中。


2. 标量替换(优化力度最大)

核心思想:

对象可以拆,就没必要存在。

做法:

  • 把对象拆成多个基本类型变量
  • 对象本身彻底消失

效果:

  • 零对象创建
  • 零 GC 压力

高频点

  1. 什么是标量替换?

    一句话答案:

    把对象拆成基本类型,彻底消除对象。


3. 锁消除

适用场景:

  • 锁对象不逃逸

  • 没有多线程竞争

JIT 判断后:

直接移除 synchronized 锁。


高频点

  1. JVM 为什么能安全地消除锁?

    一句话答案:

    因为逃逸分析确认不存在并发访问。


总结

逃不逃,看范围
不进堆,GC 少
栈分配、标量换、锁直接消


八、JVM 调优与问题排查


1. 常见 OOM 类型(先会分类)

  1. Java heap space

    一句话含义:

    堆中对象过多,GC 回收不过来。

    常见原因:

    • 内存泄漏
    • 大对象加载
    • 缓存无限增长

  2. Metaspace

    一句话含义:

    类加载太多,元空间被打满。

    常见原因:

    • 动态代理 / CGLIB 过多
    • 类加载器无法回收
    • 热部署、插件化使用不当

  3. GC overhead limit exceeded

一句话含义:

GC 拼命干活,但几乎回收不到内存。

本质判断:

系统已经处在"濒死状态"


高频点

  1. OOM 常见有哪些类型?

    一句话答案:

    堆、元空间、GC 过载。


2. 排查 OOM 的标准流程

这一步比"调参"重要十倍。


标准四步法

  1. 确认 OOM 类型

    先判断是堆、元空间,还是 GC 问题

  2. Dump 内存快照

    OOM 时或手动触发 Heap Dump

  3. 分析对象引用链

    找出占用最多、不可回收的对象

  4. 定位代码根因

    回到代码,解决引用问题,而不是只扩容


高频点

  1. 线上 OOM 你怎么排查?

    一句话答案:

    先定类型,再看 Dump,最后回到代码。


3. 常用 JVM 排查工具

不要求你精通,但必须知道用来干什么


  1. jps

    • 查看 JVM 进程

    • 快速确认目标进程 ID

    一句话:

    找 JVM 进程用的。


  2. jstack

    • 查看线程栈

    • 排查死锁、线程阻塞

    一句话:

    看线程状态。


  3. jmap

    • Dump 堆内存
    • 查看对象分布

    一句话:

    看堆里的对象。


  4. jvisualvm

    • 图形化监控
    • 内存、线程、GC 情况

    一句话:

    本地/测试环境快速分析。


  5. arthas

    • 线上诊断
    • 方法调用、参数、返回值
    • 无需重启

    一句话:
    线上救火神器。


总结

OOM 先分型

排查四步走

jstack 看线程,jmap 看对象,arthas 救线上


九、总结

  • JVM 不是简单的虚拟机,而是一个持续做运行期优化的系统

  • GC 的本质不是"回收内存",而是分析对象之间的引用关系

  • 内存泄漏不是没 GC,而是对象仍在 GC Roots 引用链上

  • 逃逸分析是 JIT 性能优化的前提,没有它就没有高效优化

  • G1 通过 Region 化和按价值回收,成为当前服务端主流选择


相关推荐
java1234_小锋2 小时前
Python常见面试题:Python是如何进行内存管理的?
java·jvm·python
Coder_Boy_3 小时前
从Java虚拟机到分布式中间件:高并发体系全解析(含电商实践细节)
java·jvm·分布式·spring·中间件
6+h3 小时前
【Java】JDK、JRE、JVM三者最通俗的讲解
java·jvm·python
小年糕是糕手4 小时前
【35天从0开始备战蓝桥杯 -- Day2】
开发语言·jvm·数据库·c++·程序人生·考研·蓝桥杯
小年糕是糕手4 小时前
【35天从0开始备战蓝桥杯 -- Day1】
jvm·数据结构·c++·程序人生·算法·职场和发展·蓝桥杯
Derek_Smart9 天前
从一次 OOM 事故说起:打造生产级的 JVM 健康检查组件
java·jvm·spring boot
大道至简Edward10 天前
深入 JVM 核心:一文读懂 Class 文件结构(附 Hex 实战解析)
jvm
weisian15113 天前
JVM--20-面试题6:如何判断对象可以被垃圾回收?
jvm·可达性算法
蚊子码农13 天前
每日一题--JVM线程分析与死锁排查
jvm