Skip to content

JVM 知识文库 — 批次 1 实施计划

目标:完成 01 字节码、02 类加载、03 运行时数据区 共 19 篇笔记 原则:原理优先 → 图解驱动 → 实战验证 → 面试佐证 设计文档:**JVM文库设计文档**


文件清单

avazoe/JVM/
├── 01-字节码与Class文件/
│   ├── 00-字节码索引.md
│   ├── 01.1-Class文件结构.md
│   ├── 01.2-字节码指令集概览.md
│   ├── 01.3-实战javap反汇编分析.md
│   └── 01.4-ASM与Javassist字节码操作入门.md
├── 02-类加载机制/
│   ├── 00-类加载索引.md
│   ├── 02.1-类加载生命周期.md
│   ├── 02.2-双亲委派模型与打破.md
│   ├── 02.3-自定义类加载器实战.md
│   ├── 02.4-Tomcat与Spring类加载分析.md
│   └── 02.5-OSGI与SPI模块化.md
└── 03-运行时数据区/
    ├── 00-运行时数据区索引.md
    ├── 03.1-程序计数器.md
    ├── 03.2-Java虚拟机栈.md
    ├── 03.3-本地方法栈.md
    ├── 03.4-Java堆.md
    ├── 03.5-方法区与元空间.md
    ├── 03.6-运行时常量池.md
    └── 03.7-直接内存.md

任务组 A:01-字节码与 Class 文件(5 篇)

Task A1: 00-字节码索引

创建: avazoe/JVM/01-字节码与Class文件/00-字节码索引.md

内容要点

  • Class 文件在 JVM 体系中的位置
  • 源码 → javac → Class 文件 → 类加载 → 运行时 全链路 ASCII 图
  • 4 篇子笔记导航表(含面试频率星级)
  • 核心考点速记(魔数、常量池、字节码指令 3 个最高频点)
  • 前端元数据:aliases/tags/created

验证: Wikilink 指向 01.1-01.4 均可点击


Task A2: 01.1-Class文件结构

创建: avazoe/JVM/01-字节码与Class文件/01.1-Class文件结构.md

内容要点

  • 一、这是什么:Class 文件是 JVM 的"可执行文件格式",Java 跨平台的秘密——OS 隔离层
  • 二、原理拆解(每个字段的"为什么"):
    • 魔数 0xCAFEBABE → 防伪标识,JVM 第一道校验
    • 版本号 → 向前兼容的控制机制(低版本 JVM 拒绝高版本 Class)
    • 常量池 → 为什么需要两级常量池?静态池(Class 文件)→ 运行时常量池(方法区),解耦符号引用
    • 访问标志 → 类/接口/枚举/注解的元信息
    • 类索引/父类索引/接口索引 → 类型体系的描述
    • 字段表 → 字段描述符编码规则
    • 方法表 → 方法签名 + Code 属性指向字节码
    • 属性表 → 最灵活的结构(Code、LineNumberTable、LocalVariableTable...)
  • 三、图解全景:完整 Class 文件十六进制布局图,每个字段标注偏移量
  • 四、实战验证:一个简单类的 javap -verbose 输出分析
  • 五、面试视角:高频追问表格
  • 回链:← 返回 **字节码索引**

Task A3: 01.2-字节码指令集概览

创建: avazoe/JVM/01-字节码与Class文件/01.2-字节码指令集概览.md

内容要点

  • 一、为什么用栈式指令:寄存器式(速度快但平台相关)vs 栈式(平台无关,牺牲一点速度)。JVM 选择可移植性
  • 二、指令分类原理
    • 加载/存储指令(aload/astore...)→ 操作数栈与局部变量表的数据搬运
    • 算术指令(iadd/isub...)→ 操作数栈顶两值运算
    • 类型转换指令(i2l/i2f...)→ JVM 的宽化/窄化转换规则
    • 对象操作指令(new/getfield/putfield...)→ 对象创建和字段访问
    • 控制转移指令(ifeq/goto/tableswitch...)→ 分支和循环的底层实现
    • 方法调用指令(invokevirtual/invokespecial/invokestatic/invokeinterface/invokedynamic)→ 五种调用的本质区别
  • 三、图解:栈帧操作示意图(局部变量表 ↔ 操作数栈 ↔ 方法区常量池引用)
  • 四、实战:一段 if-else/循环 代码的字节码逐行对应
  • 五、面试视角
  • 回链

Task A4: 01.3-实战javap反汇编分析

创建: avazoe/JVM/01-字节码与Class文件/01.3-实战javap反汇编分析.md

内容要点

  • 一、javap 是什么:JDK 自带的反汇编工具,从 Class 文件还原可读结构
  • 二、核心参数原理:-c(字节码)、-v(verbose 完整输出)、-p(含 private)、-s(签名)、-l(行号表)
  • 三、图解实战
    • 准备一个包含多种语法特征的类(构造器、静态方法、实例方法、synchronized、try-catch、泛型、内部类...)
    • javap -v 完整输出,逐区域标注解读
    • 构造函数中的 <init> 与静态块 <clinit> 的区别
    • 常量池条目如何被指令引用
  • 四、技巧总结:常见字节码模式的识别(i++ vs ++i 的字节码差异、字符串拼接的 StringBuilder 优化、try-finally 的异常表)
  • 五、面试视角
  • 回链

Task A5: 01.4-ASM与Javassist字节码操作入门

创建: avazoe/JVM/01-字节码与Class文件/01.4-ASM与Javassist字节码操作入门.md

内容要点

  • 一、为什么要操作字节码:AOP 底层、Lombok、动态代理、热部署、代码覆盖率工具的共性——都在改字节码
  • 二、两种范式对比
    • ASM:事件驱动模型(ClassVisitor → MethodVisitor → 逐个 visit 方法),直接操作字节码,性能极高但心智负担重
    • Javassist:源码级别 API(CtClass.addMethod("public void foo() {...}")),易用但性能稍低
  • 三、图解 ASM 事件模型:Visitor 链流程图
  • 四、实战:ASM 生成一个简单类 + Javassist 修改已有类的方法体
  • 五、面试视角
  • 回链

任务组 B:02-类加载机制(6 篇)

Task B1: 00-类加载索引

创建: avazoe/JVM/02-类加载机制/00-类加载索引.md

内容要点

  • 类加载在 JVM 启动→运行全流程中的角色
  • 类加载生命周期时间线图
  • 5 篇子笔记导航表
  • 核心考点速记(双亲委派模型、SPI打破委派、Tomcat类加载隔离)

Task B2: 02.1-类加载生命周期

创建: avazoe/JVM/02-类加载机制/02.1-类加载生命周期.md

内容要点

  • 一、生命周期 7 阶段:加载→验证→准备→解析→初始化→使用→卸载
  • 二、为什么阶段是交错的
    • 加载与验证可并行(一边读字节流一边校验)
    • 解析可延迟到首次使用前(迟绑定/lazy resolution)
    • 初始化触发时机(主动引用 vs 被动引用)
  • 三、每个阶段的"为什么"
    • 加载:字节流→方法区数据结构→堆上 Class 对象,为什么堆上也要有 Class 对象?
    • 验证:四层验证(文件格式→元数据→字节码→符号引用),为什么这么分层?
    • 准备:为类变量分配内存并赋零值,为什么是零值不是初始值?
    • 解析:符号引用→直接引用,为什么可以等到使用前?
    • 初始化:执行 <clinit>,为什么 JVM 要保证多线程安全?
  • 四、图解:7 阶段甘特图式交错时间线
  • 五、面试视角
  • 回链

Task B3: 02.2-双亲委派模型与打破

创建: avazoe/JVM/02-类加载机制/02.2-双亲委派模型与打破.md

内容要点

  • 一、模型本质:当收到类加载请求时,先委托父加载器,父加载不了才自己加载
  • 二、为什么需要向上委派
    • 安全隔离:核心类库(java.lang.*)只能由 Bootstrap 加载,防止恶意代码替换 String 类
    • 避免重复加载:全局限定名唯一确定一个类(加载器 + 类名)
  • 三、为什么需要打破
    • SPI 机制(JDBC 等):核心库定义接口,应用层提供实现,Bootstrap 加载的核心类需要调用应用层类——委派方向反了
    • 线程上下文类加载器(Thread Context ClassLoader)作为桥梁
  • 四、图解:三层 ClassLoader 层级图 + 委派调用链 + SPI 打破示意
  • 五、面试视角
  • 回链

Task B4: 02.3-自定义类加载器实战

创建: avazoe/JVM/02-类加载机制/02.3-自定义类加载器实战.md

内容要点

  • 一、为什么需要自定义:从非标准来源加载(网络、加密、数据库)、隔离同名类、热部署
  • 二、findClass vs loadClass
    • loadClass:实现双亲委派逻辑(先委托父类)
    • findClass:真正执行加载的逻辑(读取字节流→defineClass)
    • 为什么设计成两个方法?——职责分离
  • 三、defineClass 原理:字节流→JVM 内部 Class 对象的过程
  • 四、图解:自定义 ClassLoader 加载流程图
  • 五、实战:从文件系统加载加密的 Class 文件(先解密→defineClass)
  • 六、面试视角
  • 回链

Task B5: 02.4-Tomcat与Spring类加载分析

创建: avazoe/JVM/02-类加载机制/02.4-Tomcat与Spring类加载分析.md

内容要点

  • 一、Tomcat 为什么需要多级 ClassLoader
    • 多个 Web 应用部署在同一 Tomcat → 需要隔离(同名类不冲突)
    • 同时又需要共享(Servlet API、Tomcat 自身类库)
  • 二、Tomcat ClassLoader 层级
    • Bootstrap(JRE 核心)→ System(Tomcat 启动类)→ Common(共享库)→ Catalina(Tomcat 自身)→ Shared(所有应用共享)→ Webapp(每个应用独立)
    • 破坏双亲委派:Webapp 先自己加载,找不到才向上(与标准模型反向)
  • 三、Spring Boot fat jar:LaunchedURLClassLoader 如何从一个嵌套 jar 中加载类
  • 四、图解:Tomcat 多级 ClassLoader 树形结构图
  • 五、面试视角
  • 回链

Task B6: 02.5-OSGI与SPI模块化

创建: avazoe/JVM/02-类加载机制/02.5-OSGI与SPI模块化.md

内容要点

  • 一、模块化加载的本质:从层级委派到网状依赖——每个模块有自己的 ClassLoader,模块间显式声明依赖
  • 二、OSGI
    • Bundle = 独立 ClassLoader + 显式 Import/Export 包
    • 为什么可以热插拔:依赖关系是声明的,替换 Bundle 时只需重建依赖图
    • 代价:类加载复杂度剧增,调试困难(ClassNotFoundException 和 NoClassDefFoundError 频繁)
  • 三、SPI 机制详解
    • META-INF/services 文件 + ServiceLoader
    • 线程上下文类加载器如何桥接 Bootstrap→Application
    • JDBC 驱动加载的全链路追踪作为典型案例
  • 四、Java 9 模块化系统:JPMS 的 module-info.java 与 ClassLoader 的关系
  • 五、图解:传统层级委派 vs OSGI 网状依赖 vs JPMS 模块图
  • 六、面试视角
  • 回链

任务组 C:03-运行时数据区(8 篇)

Task C1: 00-运行时数据区索引

创建: avazoe/JVM/03-运行时数据区/00-运行时数据区索引.md

内容要点

  • JVM 内存全景布局图(线程私有区 × N + 线程共享区 × 1)
  • 线程私有 vs 线程共享的划分逻辑
  • 7 篇子笔记导航表
  • 核心考点:栈 vs 堆、方法区变迁(PermGen→Metaspace)、直接内存

Task C2: 03.1-程序计数器

创建: avazoe/JVM/03-运行时数据区/03.1-程序计数器.md

内容要点

  • 一、是什么:当前线程所执行字节码的行号指示器
  • 二、为什么需要:CPU 时间片轮转,线程切换后必须知道"执行到哪儿了"
  • 三、为什么唯一不 OOM:每个线程一个 PC,大小固定(一个 word 长度),生命周期与线程绑定;线程结束 PC 就释放;规范明确不规定 OOM
  • 四、native 方法时 PC 值:undefined——因为执行的是原生代码而非字节码
  • 五、图解:CPU 线程调度 → 时间片轮转 → PC 指向字节码位置
  • 六、面试视角
  • 回链

Task C3: 03.2-Java虚拟机栈

创建: avazoe/JVM/03-运行时数据区/03.2-Java虚拟机栈.md

内容要点

  • 一、栈帧是什么:每个方法调用对应一个栈帧,包含四部件
  • 二、四部件协作原理
    • 局部变量表:为什么用槽位(slot)机制?(不同类型占用不同 slot 数,long/double 占 2 个)——可复用 slot 的 GC 影响
    • 操作数栈:为什么用栈而不是寄存器?(移植性)——每个指令从栈顶取操作数、结果压回栈顶
    • 动态链接:指向运行时常量池中该方法的符号引用——为什么间接引用?(支持多态、迟绑定)
    • 返回地址:正常返回(return 指令)vs 异常返回(异常表查找),两者的区别
  • 三、StackOverflowError vs OOM:递归过深 vs 线程过多
  • 四、图解:栈帧四部件结构图 + 方法调用链帧栈变化示意
  • 五、面试视角
  • 回链

Task C4: 03.3-本地方法栈

创建: avazoe/JVM/03-运行时数据区/03.3-本地方法栈.md

内容要点

  • 一、是什么:为 native 方法服务的栈
  • 二、HotSpot 的实现:虚拟机栈和本地方法栈合二为一——为什么?(简化实现,native 方法也在同一个线程执行)
  • 三、JNI 调用原理:Java 方法 → native 方法时的栈切换和参数传递
  • 四、图解:JNI 调用栈切换示意图
  • 五、面试视角(这个话题较浅,主要作为面试冷门知识点的覆盖)
  • 回链

Task C5: 03.4-Java堆

创建: avazoe/JVM/03-运行时数据区/03.4-Java堆.md

内容要点

  • 一、为什么几乎所有对象都在堆上分配:堆是线程共享的,GC 管理的核心区域
  • 二、分代设计原理
    • 为什么分代?(弱分代假说:大多数对象朝生夕死 → Young GC 高频小范围回收 → 少数长命对象晋升 Old 区)
    • Eden : S0 : S1 = 8:1:1 的理由(经验值,90%+ 对象活不过第一次 GC)
    • 对象晋升阈值(-XX:MaxTenuringThreshold 默认 15)——为什么是 15?(对象头中 age 字段只有 4 bits)
  • 三、TLAB 原理:Thread Local Allocation Buffer——为什么线程分配对象需要本地缓冲?(避免多线程竞争同一块堆内存时的 CAS 自旋开销)
  • 四、字符串常量池:从 PermGen 移到 Heap 的变迁(JDK 7)
  • 五、图解:堆分代结构全景图 + 对象流动方向(Eden → S0/S1 → Old)+ TLAB 在 Eden 中的位置
  • 六、面试视角
  • 回链

Task C6: 03.5-方法区与元空间

创建: avazoe/JVM/03-运行时数据区/03.5-方法区与元空间.md

内容要点

  • 一、方法区是什么:存储类信息(类结构、方法数据、字段数据、运行时常量池、JIT 编译缓存)
  • 二、PermGen 时代的痛点
    • 为什么 PermGen OOM 频发?(固定大小、String.intern() 动态增长、类卸载困难)
    • -XX:MaxPermSize 设置的困境
  • 三、JDK 8 为什么迁移到 Metaspace
    • 移到直接内存 → 默认不设上限,自动扩缩
    • 类元数据与 Java 堆解耦 → 堆 GC 不影响类元数据
    • String 常量池搬到堆 → 堆 GC 自动回收字符串
  • 四、图解:PermGen 时代内存布局 vs Metaspace 时代内存布局对比图
  • 五、面试视角
  • 回链

Task C7: 03.6-运行时常量池

创建: avazoe/JVM/03-运行时数据区/03.6-运行时常量池.md

内容要点

  • 一、为什么需要两套常量池:Class 文件常量池是静态模板(编译时确定),运行时常量池是动态实例(可运行时添加——String.intern())
  • 二、符号引用 → 直接引用
    • 符号引用:CONSTANT_Class_info、CONSTANT_Methodref_info...(字符串描述)
    • 直接引用:内存地址或偏移量(运行时确定)
    • 为什么不在编译时直接确定?(动态链接、多态——实际类型可能运行时才知道)
  • 三、String.intern() 原理:运行时向常量池动态添加字符串
  • 四、图解:Class 常量池 → 类加载 → 运行时常量池 转换对比图
  • 五、面试视角
  • 回链

Task C8: 03.7-直接内存

创建: avazoe/JVM/03-运行时数据区/03.7-直接内存.md

内容要点

  • 一、为什么需要直接内存:传统 I/O 路径——磁盘→OS 内核缓冲区→JVM 堆缓冲区→应用程序,两次复制。直接内存 I/O——磁盘→OS 内核缓冲区→直接内存(堆外)→应用程序,零次复制
  • 二、DirectByteBuffer 原理
    • Java 堆上只有一个"引用对象"(含 address 字段指向堆外内存)
    • 实际数据存储在堆外
  • 三、回收机制
    • Cleaner 虚引用:DirectByteBuffer 被 GC 时,Cleaner 触发 Deallocator 释放堆外内存
    • 为什么不能用 finalize?(太慢、不可靠)
    • 手动回收:sun.misc.Unsafe.freeMemory() 或 ((DirectBuffer)buffer).cleaner().clean()
  • 四、图解:堆 I/O vs 直接内存 I/O 数据路径对比图
  • 五、面试视角
  • 回链

验证清单

每篇笔记完成后检查:

  • [ ] Frontmatter 完整(aliases/tags/created)
  • [ ] 所有 [[wikilink]] 指向的文件存在
  • [ ] ASCII 图表对齐无误
  • [ ] 代码块标注语言类型
  • [ ] 底部有回链 ← 返回 [[00-XX索引]]
  • [ ] 同板块内笔记互链正确

执行顺序

A1 (索引) → A2 → A3 → A4 → A5   [01-字节码完成]
B1 (索引) → B2 → B3 → B4 → B5 → B6  [02-类加载完成]
C1 (索引) → C2 → C3 → C4 → C5 → C6 → C7 → C8  [03-运行时数据区完成]

每个板块内先建索引页,后续笔记的 wikilink 才能全部生效。


计划创建时间: 2026-06-15

Knowledge4J — Java 知识库