JVM规范第 2章:从 class 文件到运行时数据区
2026/8/24 14:08:08 网站建设 项目流程

本教程基于 Oracle 官方《The Java Virtual Machine Specification》(Java SE 26)第 2 章《The Structure of the Java Virtual Machine》整理编写,面向想真正理解 JVM 内部如何工作的 Java 开发者。读完后,你将能用自己的话讲清楚:一段 Java 代码编译后变成了什么、JVM 在运行时把数据放在哪里、方法是怎么被调用和返回的。

你将学到

  • JVM 是什么「抽象机器」,它和具体实现的关系
  • class文件格式的定位(它为什么与平台无关)
  • JVM 的两大类数据类型:基本类型 vs 引用类型
  • 运行时数据区:程序计数器、虚拟机栈、堆、方法区、常量池、本地方法栈
  • **栈帧(Frame)**的内部结构:局部变量表、操作数栈、动态链接
  • 对象、浮点、特殊方法(<init>/<clinit>)、异常与指令集的要点

前置知识

  • 写过 Java,知道「编译 → 运行」的基本流程
  • 了解栈(stack)和堆(heap)这两个基本数据结构概念
  • 不需要读过后面的 JVM 规范章节;本篇就是第 2 章的通俗版

一、先建立心智模型:JVM 是一台「抽象机器」

规范开篇就说得很直白:JVM 描述的是一台抽象机器,而不是某一份具体实现

这意味着:

  • 任何「正确」的 JVM 实现,只要能读取class文件格式、并正确执行其中规定的操作,就合规。
  • 至于运行时数据区在内存里怎么摆、用哪种垃圾回收算法、指令内部怎么优化(比如 JIT 编译成机器码)——全由实现者自己决定
  • 所有 Unicode 引用基于The Unicode Standard, Version 17.0.0

一句话理解:规范规定了「输入输出契约」,没规定「屋子内部怎么装修」。这也是为什么 HotSpot、OpenJ9 跑起来行为一致,但底层天差地别。


二、class文件格式:与平台无关的二进制

被 JVM 执行的编译后代码,用一种与硬件和操作系统无关的二进制格式表示,通常(但不一定)存成文件,这就是class文件格式。

它精确定义了类或接口的表示,连字节序(big-endian,大端)这种在普通平台相关目标文件里常被忽略的细节都写死了。完整的class文件结构在第 4 章详述,本篇先记住它的定位即可:

class文件是 JVM 的「公共设计接口」之一——你写的.javajavac翻译成.class,JVM 只认.class


三、数据类型:JVM 操作的两大类

JVM 只操作两类类型:

类别对应的值例子
基本类型(primitive types)primitive valuesintlongfloatbooleanreturnAddress
引用类型(reference types)reference values类实例、数组、接口实例、null

关键设计点:

  • 几乎所有类型检查都在运行前完成(通常由javac编译器做)。JVM 自己不需要在运行时给基本类型值贴类型标签。
  • 指令集靠「专用指令」区分操作数类型。比如加法就有四个不同指令:iaddint)、laddlong)、faddfloat)、dadddouble)。
  • JVM显式支持对象:对象要么是动态分配的类实例,要么是数组。指向对象的引用类型叫reference,且具有多态性。

3.1 基本类型细分

整数类型(Integral Types)
类型位数取值范围默认值
byte8[-128, 127]0
short16[-32768, 32767]0
int32[-2147483648, 2147483647]0
long64[-9223372036854775808, 9223372036854775807]0
char16[0, 65535](UTF-16 的 BMP 代码点,无符号'\u0000'

注意char无符号的,这点常被人忽略。

浮点类型(Floating-Point Types)
  • float:对应 IEEE 754 binary32;默认正零
  • double:对应 IEEE 754 binary64;默认正零
  • Java SE 15 起采用 IEEE 754 2019 版(之前是 1985 版)
  • 包含正零/负零、正无穷/负无穷、NaN(Not a Number,用于表示 0/0 这类无效操作)

浮点参数要点:

  • float:尾数 24 位、指数 8 位;double:尾数 53 位、指数 11 位
  • NaN是无序的:任何与NaN的比较都返回 false
  • 正零 == 负零,但除法结果不同:1.0/0.0 = 正无穷1.0/-0.0 = 负无穷
returnAddress类型
  • jsrretjsr_w指令使用,值是「指向某条 JVM 指令操作码」的指针
  • 不直接对应任何 Java 语言类型,运行期间不可被程序修改
  • (现代 Java 代码基本不会直接碰到它,了解即可)
boolean类型——JVM 的「半吊子」支持

JVM定义了boolean,但只给了有限支持

  • 没有专门的boolean指令;boolean表达式会被编译器编译成int操作(0=false,非 0=true)
  • boolean数组被直接支持:用newarray创建,用baload/bastore访问
  • 在 Oracle 实现里,boolean数组按byte数组编码(每元素 8 位),1=true、0=false;当映射成int时,编译器必须用同样的编码

这也是为什么boolean只占 1 字节(数组里),但作为局部变量/字段时常被 JVM 当int处理——规范把灵活度留给了实现。

3.2 引用类型

三类引用类型:

  • class types:指向动态创建的类实例
  • array types:由 component type 递归构成,最终得到 element type(必须是基本类型、类或接口)
  • interface types:指向实现了该接口的类实例或数组

特殊值null

  • 是默认的引用值,没有运行时类型
  • 可以转换成任意引用类型
  • 规范不规定null在内存里怎么编码(各家实现不同)

四、运行时数据区(Run-Time Data Areas)—— 本篇核心

JVM 在运行程序时,把数据放在若干「区」里。有的区随 JVM 启动而创建、退出时销毁;有的是每线程私有的。

┌─────────────────────────────────────────┐ 每个线程私有 → │ pc 寄存器 │ JVM 栈(帧) │ 本地方法栈 │ ├─────────────────────────────────────────┤ 所有线程共享 → │ 堆 (Heap) │ 方法区(含常量池) │ └─────────────────────────────────────────┘

4.1pc寄存器(程序计数器)

  • 每个线程都有自己的程序计数器
  • 如果当前方法不是 native:存「下一条要执行的指令」的地址
  • 如果是 native 方法:值未定义
  • 宽度要足够容纳returnAddress或原生指针

为什么每个线程都有?因为多线程是轮流占用 CPU 的,线程切回来时得知道自己执行到哪了。

4.2 Java 虚拟机栈(JVM Stacks)

  • 每线程私有,用来存「栈帧(frames)」
  • 类似 C 语言的栈:保存局部变量和中间结果
  • 大小可以固定,也可以动态扩展
  • 溢出时抛:StackOverflowError(栈太深,比如无限递归)或OutOfMemoryError(想扩展却没内存)

4.3 堆(Heap)

  • 所有线程共享,是类实例和数组分配内存的地方
  • JVM 启动时创建,由垃圾回收器(GC)自动管理
  • 可固定大小也可扩展
  • 内存不够时抛OutOfMemoryError

几乎所有你new出来的对象,都住在这里。

4.4 方法区(Method Area)

  • 线程共享,存类的结构信息:运行时常量池、字段/方法数据、方法的字节码等
  • 逻辑上属于堆的一部分,但实现可以选择「不回收它」
  • 内存不够时抛OutOfMemoryError

4.5 运行时常量池(Run-Time Constant Pool)

  • 每个类/接口都有一个,是class文件里constant_pool运行时表示
  • 内容包含:编译期已知的常量、以及运行期才解析的符号引用(比如一个方法的名字)
  • 从方法区分得内存;构建时不够就抛OutOfMemoryError

4.6 本地方法栈(Native Method Stacks)

  • 给 native 方法(比如用 C 写的)用的栈,每个线程可选地分配
  • 异常规则和 JVM 栈一样:StackOverflowError/OutOfMemoryError

五、栈帧(Frames):方法调用的载体

每次方法被调用,JVM 就新建一个 frame(栈帧)。它装着:

  • 方法的局部数据
  • 部分计算结果
  • 动态链接信息
  • 返回值 / 异常分发的去处

frame 从当前线程的 JVM 栈里分配,包含:

  1. 局部变量数组(Local Variables)
  2. 操作数栈(Operand Stack)
  3. 指向「当前类运行时常量池」的引用

注意:只有「当前帧(current frame)」是活跃的,线程之间不共享帧。

5.1 局部变量表(Local Variables)

  • 大小在编译期就确定了
  • 一个「变量槽(slot)」能存int/float/reference/returnAddress
  • longdouble因为 64 位,占两个槽
  • 实例方法调用时,变量 0 固定是this,参数从 1 开始;而类方法(static)参数从 0 开始

5.2 操作数栈(Operand Stack)

  • 一个LIFO(后进先出)栈,最大深度编译期定好
  • 用途:加载常量、做运算、给方法传参
  • long/double在操作数栈里占两个单元
  • 类型必须匹配:不能把两个int当成一个long来用

直觉理解:字节码指令大多从操作数栈「弹出」操作数、算完再「压回」结果。它就像手工计算器里的暂存栈。

5.3 动态链接(Dynamic Linking)

  • 帧会引用「当前方法所在类型的运行时常量池」
  • 符号引用(比如「某个叫 foo 的方法」)在运行时转换成具体的方法引用、字段偏移量
  • 这就是 Java 能支持「晚期绑定(late binding)」的基础

5.4 方法调用如何「完成」

  • 正常完成(Normal):方法没抛异常,执行return系列指令,把返回值交给调用者,并恢复调用者帧的状态。
  • 突然完成(Abrupt):方法里抛了异常又没自己处理,则不返回值;当前帧被弹出,异常沿调用链往上抛。

六、对象的表示

规范不强制对象在内存里的内部结构——又一处「留给实现」。

在 Oracle 的某些实现里:

  • 类实例的引用,是一个「指向 handle 的指针」
  • 这个 handle 里包含:方法表指针、Class对象指针、以及指向堆中真实数据的指针

这解释了为什么 Java 里「对象引用」传递,传递的是引用本身,而非对象拷贝。


七、浮点运算注意事项

JVM 采用 IEEE 754 的一个子集(Java SE 15+ 用 2019 版)。与严格 IEEE 754 的主要差异:

  • drem/frem(取余)基于向零舍入,而非「最近」
  • dneg/fneg(取负)不要求反转 NaN 的符号位
  • 不抛IEEE 异常(如溢出)
  • 没有signaling NaN
  • 不支持extended 格式(如 80 位扩展精度)

舍入策略(Rounding Policies)

  • round to nearest(就近舍入):除「转整数」和「取余」外,选最近的可表示值(平局时取末位为 0 的那个)
  • round toward zero(向零舍入 / 截断):用于d2i/f2i等转换,以及drem/frem

值集(Value Sets)小史

  • Java 1.2–16 允许使用 extended-exponent 值集
  • Java SE 17 起强制 strict evaluationACC_STRICT标志不再影响结果

八、特殊方法(Special Methods)

JVM 里有几类「名字特殊」的方法,需要单独记:

8.1 实例初始化方法<init>

  • (非接口)中,名为<init>且返回void的方法
  • 只能由invokespecial调用,且作用于「尚未初始化完成的实例」
  • 对应你在 Java 里写的每个构造器

8.2 类初始化方法<clinit>

  • 名为<clinit>且返回void
  • class 文件版本 ≥ 51 时,必须带ACC_STATIC标志且无参数
  • JVM 在类初始化阶段自动调用(对应 Java 里的static { }块和静态变量赋值)

8.3 签名多态方法(Signature Polymorphic Methods)

  • 声明在java.lang.invoke.MethodHandleVarHandle
  • 只有一个Object[]参数,带ACC_VARARGS+ACC_NATIVE
  • invokevirtual做特殊处理(这是MethodHandle.invoke能「看起来像普通调用」的底层魔法)

九、异常(Exceptions)

  • 异常是Throwable的子类实例
  • 三种被抛出的原因:
    1. athrow指令显式抛出
    2. 同步异常:JVM 指令检测到(如数组越界ArrayIndexOutOfBoundsException
    3. 异步异常:JVM 内部错误等
  • 异常处理器按class文件里的表顺序匹配;没命中就「突然完成」并沿调用链上抛;线程终止前交给 uncaught exception handler
  • JVM 的异常是「精确的(precise)」:抛出点之前的效果可见,之后的不可见

十、指令集摘要

JVM 指令集的「长相」:

  • 一条指令 =1 字节操作码(opcode)+ 若干操作数
  • 操作数**大端(big-endian)**存放
  • lookupswitch/tableswitch外,所有指令单字节对齐
  • 解释器循环就是:取操作码 → 取操作数 → 执行

类型如何编码进指令

多数指令把类型信息直接编进操作码

  • i= int,l= long,f= float,d= double,a= reference(引用)
  • byte/short/char/boolean加载时统一转成int(符号扩展或零扩展),所以它们在 JVM 里其实都是当int算的

指令家族一览

类别代表指令说明
Load/Storeiload/istore/wide局部变量 ↔ 操作数栈,索引过大时用wide
算术iadd/imul/ishl/icmp整数除零抛ArithmeticException;浮点不抛
类型转换i2l(宽化)/i2b(窄化)宽化不丢精度;窄化可能丢,向零舍入
对象创建new/newarray/getfield/arraylength
操作数栈管理pop/dup/swap复制、交换栈顶
控制转移ifeq/goto/tableswitchint比较指令最丰富
方法调用invokevirtual/invokeinterface/invokespecial/invokestatic/invokedynamic返回用ireturn
抛异常athrow显式抛出
同步ACC_SYNCHRONIZED(方法级)/monitorenter+monitorexit(块级)管程锁,结构化规则

十一、公共设计,私有实现

这一节是整章的「哲学总结」:

  • 公共视图class文件格式 + 指令集 + 运行时语义
  • 私有实现:JIT 把字节码翻译成本地机器码、各种 GC 算法、内存布局……只要对外行为和语义不变,实现者想怎么优化都行
  • 调试器、性能分析器等工具有时需要看「引擎盖下的东西」,Oracle 也在推动通用接口(如 JVM TI)来标准化这种访问

理解这一点,你就能看懂为什么「同一份.class,在不同 JVM 上跑得一样快/慢不一样」——快慢在实现,行为在规范。


练习

练习 1(基础)char类型的取值范围是多少?它和short在「有无符号」上有何本质区别?

练习 2(理解堆与栈):下面这段代码的user对象本身、以及user这个引用变量,分别存在哪个运行时数据区?

publicvoiddemo(){Useruser=newUser();// User 是一个普通类}

练习 3(指令集):要把一个int局部变量加载到操作数栈,应该用哪条指令?如果要把int转成long,又是什么指令?

练习 4(方法调用):Java 里的static { ... }静态初始化块,在 JVM 层面对应哪个特殊方法?它能被你写的 Java 代码直接调用吗?

练习 5(开放思考):为什么规范说「JVM 异常是精确的」很重要?如果异常不精确,对调试会有什么影响?

  1. char范围 [0, 65535],是无符号16 位;short有符号16 位,范围 [-32768, 32767]。同样 16 位,解释方式不同。
  2. new User()创建的对象实例user这个引用变量demo()方法的局部变量,在当前线程的 JVM 栈帧的局部变量表里。
  3. 加载int局部变量:iload(带索引,如iload 1);intlong转换:i2l
  4. 对应<clinit>(类初始化方法)。不能被 Java 代码直接调用——它由 JVM 在类初始化阶段自动调用。
  5. 精确异常意味着「异常抛出点之前的所有副作用都生效、之后的都不生效」,这样调试时栈轨迹能准确告诉你是哪一行出的错;若不精确,你看到的异常位置会和实际出错点错位,极难排查。

小结

这一章给你搭起了 JVM 的「骨架」:

  • 输入契约class文件(与平台无关)
  • 数据类型:基本类型(含boolean的半支持、returnAddress)与引用类型(null是默认值)
  • 运行时布局:pc 寄存器、JVM 栈、堆、方法区、常量池、本地方法栈
  • 方法执行:靠栈帧(局部变量 + 操作数栈 + 动态链接)完成,有正常/突然两种结束方式
  • 其它要点:对象表示、浮点约定、<init>/<clinit>/签名多态方法、精确异常、1 字节操作码指令集

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询