news 2026/8/25 14:58:41

JVM规范第 3 章:从 Java 源码到字节码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM规范第 3 章:从 Java 源码到字节码

本教程基于 Oracle 官方《The Java Virtual Machine Specification》(Java SE 26)第 3 章《Compiling for the Java Virtual Machine》整理编写。这一章本身不是规范性内容,而是由 Oracle 的javac示例展示「Java 源码是怎么变成 JVM 指令的」。学完这篇,你再看到javap -c反编译出来的字节码,就不会一头雾水了。

前置知识

  • 机器上装了 JDK(需要javacjavap
  • 知道「栈(后进先出)」是什么

一、先学会看「虚拟机汇编」

示例都是「Java 源码 + 编译后的字节码清单」成对出现。字节码用javap输出的「虚拟机汇编语言」表示,每行格式是:

<偏移> <操作码> [ <操作数1> [ <操作数2>... ]] [// 注释]

例如:

8 bipush 100 // Push int constant 100
  • 8:这条指令在方法字节码数组里的偏移量
  • bipush:**操作码(opcode)**助记符
  • 100:操作数
  • 后面是注释

运行时常量池的引用用#前缀:

10 ldc #1 // 从常量池 #1 推入 float 常量 9 invokevirtual #4 // 调用 Example.addTwo(II)I

后面所有示例都遵循这个格式。先看偏移、再看 opcode、最后看注释,很快就能形成直觉。


二、常量、局部变量与控制结构:JVM 是「栈机器」

最重要的心智模型:JVM 是基于栈的。大多数操作从「当前帧的操作数栈」弹出操作数、算完再把结果压回去。方法被调用时会新建一个帧,自带一个操作数栈和一组局部变量。

2.1 一个for循环在字节码里长什么样

voidspin(){inti;for(i=0;i<100;i++){;// 循环体为空}}

编译为:

0 iconst_0 // 把 int 常量 0 压栈 1 istore_1 // 存到局部变量 1(i = 0) 2 goto 8 // 第一次先跳过自增 5 iinc 1 1 // 局部变量 1 自增 1(i++) 8 iload_1 // 把局部变量 1 压栈(i) 9 bipush 100 // 把 int 常量 100 压栈 11 if_icmplt 5 // 若 i < 100,跳回 5 继续 14 return // 否则返回

观察几个细节:

  • 常量0iconst_0(带「隐式操作数」的专用指令);100bipush(单字节立即数)
  • iint,存在局部变量槽 1istore_1/iload_1
  • 循环条件i < 100被编译成「压栈 i → 压栈 100 →if_icmplt比较跳转」

2.2double循环:没有dinc

voiddspin(){doublei;for(i=0.0;i<100.0;i++){;}}

关键片段:

5 dload_1 6 dconst_1 7 dadd // 用 dadd 做自增,因为【没有 dinc 指令】 8 dstore_1 ... 13 dcmpg // 浮点比较用 dcmpg 14 iflt 5 // 配合 iflt,没有 if_dcmplt
  • double两个局部变量槽
  • 自增不能像int那样iinc,只能用dload+dconst_1+dadd+dstore
  • 浮点比较没有「带条件的比较跳转」,而是dcmpg先比较、再配合iflt这类通用跳转

2.3short循环:i2s截断

voidsspin(){shorti;for(i=0;i<100;i++){;}}
5 iload_1 6 iconst_1 7 iadd 8 i2s // 把 int 截断回 short 9 istore_1 ...

规范早就说过:byte/char/short在 JVM 里都是当int算的(之前第 2 章讲过)。所以自增在int上做完,再用i2s截断回short存回去。


三、算术:位运算的「另类写法」

intalign2grain(inti,intgrain){return((i+grain-1)&~(grain-1));}
0 iload_1 // i 1 iload_2 // grain 2 iadd // i + grain 3 iconst_1 4 isub // ... - 1 5 iload_2 // grain 6 iconst_1 7 isub // grain - 1 8 iconst_m1 // -1 9 ixor // ~(grain-1) 用 (-1) XOR (grain-1) 实现 10 iand // 与上一步结果 11 ireturn

注意~x(按位取反)在字节码里是用-1 XOR xiconst_m1+ixor)实现的——这是编译器常用技巧。


四、访问运行时常量池:小常量走「快车道」

把不同大小的常量推入栈,JVM 给了不同的指令:

常量类型指令
int(-1、0、1、2、3、4、5)iconst_<i>(如iconst_0
单字节intbipush
两字节intsipush
int/float/Stringldc/ldc_w(从常量池取)
long/doublelconst_<l>/dconst_<d>ldc2_w

示例:

voiduseManyNumeric(){inti=100;intj=1000000;longl1=1;longl2=0xffffffff;// 即 int 的 -1doubled=2.2;}
0 bipush 100 // 小 int 用 bipush 2 istore_1 3 ldc #1 // 大 int 用 ldc(从常量池 #1 取 1000000) 5 istore_2 6 lconst_1 // 很小的 long 用 lconst_1 7 lstore_3 8 ldc2_w #6 // long 0xffffffff 用 ldc2_w 11 lstore 5 13 ldc2_w #8 // double 2.2 用 ldc2_w 16 dstore 7

经验:编译器会「能省则省」——够小的常量用专用短指令,大了才往常量池塞。


五、更多控制流:while循环

voidwhileInt(){inti=0;while(i<100){i++;}}
0 iconst_0 1 istore_1 2 goto 8 // 先跳到条件判断,避免首轮多自增一次 5 iinc 1 1 // 循环体:i++ 8 iload_1 9 bipush 100 11 if_icmplt 5 // 条件成立跳回 5 14 return

whilefor编译出来几乎一样——测试在底部goto先跳过首轮自增。


六、接收参数:this占的槽位

很重要的一条规则:

  • 实例方法:参数从局部变量槽1开始,槽0永远是this
  • 静态方法:没有this,参数从槽0开始
intaddTwo(inti,intj){returni+j;}// 实例方法staticintaddTwoStatic(inti,intj){returni+j;}// 静态方法
Method int addTwo(int,int) 0 iload_1 // 实例方法:第 1 个参数在槽 1 1 iload_2 // 第 2 个参数在槽 2 2 iadd 3 ireturn Method int addTwoStatic(int,int) 0 iload_0 // 静态方法:第 1 个参数在槽 0 1 iload_1 2 iadd 3 ireturn

七、方法调用:四条invoke*指令

指令用途特点
invokevirtual实例虚方法操作数指向常量池里的符号引用
invokestatic静态方法不传this
invokespecial父类方法、构造器<init>跳过虚分派
invokeinterface接口方法本例未展开

调用实例方法(注意要先aload_0this压栈):

intadd12and13(){returnaddTwo(12,13);}
0 aload_0 // 压入 this 1 bipush 12 3 bipush 13 5 invokevirtual #4 // Method Example.addTwo(II)I 8 ireturn

调用父类方法用invokespecial

classFarextendsNear{intgetItFar(){returnsuper.getItNear();}}
0 aload_0 1 invokespecial #4 // Method Near.getItNear()I(不虚分派) 4 ireturn

八、对象与字段:new必须配invokespecial <init>

创建一个对象的标准三步:

Objectcreate(){returnnewObject();}
0 new #1 // 1) 在堆上创建实例,引用压栈 3 dup // 2) 复制栈顶引用(一份给 <init>,一份留给返回) 4 invokespecial #4 // 3) 调用 <init> 初始化(注意不是 invokevirtual!) 7 areturn // 返回栈上那个引用

dup是关键:<init>会「消费」一份引用,但方法最终要areturn把新对象返回,所以需要两份。

字段读写用getfield/putfield(都通过常量池符号引用定位):

voidsetIt(intvalue){i=value;}intgetIt(){returni;}
Method void setIt(int) 0 aload_0 // this 1 iload_1 // value 2 putfield #4 // Field Example.i I 5 return Method int getIt() 0 aload_0 1 getfield #4 4 ireturn

九、数组:newarray/anewarray/multianewarray

voidcreateBuffer(){intbuffer[];intbufsz=100;intvalue=12;buffer=newint[bufsz];buffer[10]=value;value=buffer[11];}
6 iload_2 // bufsz 7 newarray int // 创建 int[](基本类型数组用 newarray) 9 astore_1 10 aload_1 11 bipush 10 13 iload_3 14 iastore // buffer[10] = value 15 aload_1 16 bipush 11 18 iaload // value = buffer[11] 19 istore_3

多维数组用multianewarray

int[][][]create3DArray(){intgrid[][][];grid=newint[10][5][];returngrid;}
0 bipush 10 2 iconst_5 3 multianewarray #1 dim #2 // 前两维是 10 和 5,第三维留 null 7 astore_1 8 aload_1 9 areturn

十、switch:密集用tableswitch,稀疏用lookupswitch

switch只支持int(其它类型编译期会被提升为int)。

密集连续的 case →tableswitch

intchooseNear(inti){switch(i){case0:return0;case1:return1;case2:return2;default:return-1;}}
0 iload_1 1 tableswitch 0 to 2: 0: 28 1: 30 2: 32 default:34 28 iconst_0 29 ireturn 30 iconst_1 ...

稀疏分散的 case →lookupswitch(表必须按 key 排序):

intchooseFar(inti){switch(i){case-100:return-1;case0:return0;case100:return1;default:return-1;}}
0 iload_1 1 lookupswitch 3: -100: 36 0: 38 100: 40 default: 42

直觉:tableswitch像「用 case 值当下标直接跳」,lookupswitch像「查排序表做二分」。编译器按 case 是否密集自动选。


十一、操作数栈操作:dup2_x1

栈操作指令把栈上的值当「无类型」处理,但不会拆开一个long/double(它们占两个单元)。

publiclongnextIndex(){returnindex++;}privatelongindex=0;
0 aload_0 1 dup 2 getfield #4 // 取出当前 index 5 dup2_x1 // 复制并插入栈下方,为「返回旧值」和「自增后写回」做准备 6 lconst_1 7 ladd 8 putfield #4 // index = index + 1 写回 11 lreturn // 返回旧值

十二、异常:throwtry-catch

throw new X()编译成「new+dup+invokespecial <init>+athrow」:

voidcantBeZero(inti)throwsTestExc{if(i==0){thrownewTestExc();}}
0 iload_1 1 ifne 12 // 若 i != 0 跳过 4 new #1 // new TestExc 7 dup 8 invokespecial #7 // <init> 11 athrow // 抛出 12 return

try-catch不靠特殊跳转,而是靠异常表(Exception table)

voidcatchOne(){try{tryItOut();}catch(TestExce){handleExc(e);}}
0 aload_0 1 invokevirtual #6 // tryItOut() 4 return 5 astore_1 // 若抛出异常,跳到这里:e = 异常对象 6 aload_0 7 aload_1 8 invokevirtual #5 // handleExc(e) 11 return Exception table: From To Target Type 0 4 5 Class TestExc

异常表含义:在偏移0~4(含头不含尾)之间若抛出TestExc,就跳到偏移5开始处理。多 catch、嵌套 catch 都是用多条异常表项表达的。


十三、finallyjsr/ret子例程(旧版本)

注:这是 class 文件版本 ≤ 50.0(即 Java 7 及以前)的做法。现代 Java 用StackMapTable+ 复制 finally 代码块的方式,不再用jsr/ret(还记得第 2 章说returnAddress是给jsr/ret用的吗?这就是它的归宿)。

voidtryFinally(){try{tryItOut();}finally{wrapItUp();}}
0 aload_0 1 invokevirtual #6 // tryItOut() 4 jsr 14 // 正常路径:跳去执行 finally 7 return 8 astore_1 // 异常路径:异常存起来 9 jsr 14 // 同样去执行 finally 12 aload_1 13 athrow // finally 跑完后把异常重新抛出 14 astore_2 // finally 子例程入口 15 aload_0 16 invokevirtual #5 // wrapItUp() 19 ret 2 // 用 returnAddress 返回调用点 Exception table: From To Target Type 0 4 8 any

十四、同步:monitorenter/monitorexit

synchronized块的编译:进入时monitorenter,退出时monitorexit,并且编译器会保证每条退出路径都有配对的monitorexit(通过异常表兜底)。

voidonlyMe(Foof){synchronized(f){doSomething();}}
0 aload_1 1 dup 2 astore_2 // 把 f 的引用存一份,供释放时用 3 monitorenter // 加锁 4 aload_0 5 invokevirtual #5 // doSomething() 8 aload_2 9 monitorexit // 正常释放 10 goto 18 13 astore_3 // 异常路径:把异常存起来 14 aload_2 15 monitorexit // 异常时也释放锁(关键!避免死锁) 16 aload_3 17 athrow // 再抛出 18 return Exception table: From To Target Type 4 10 13 any 13 16 13 any

synchronized方法更简单——不需要显式指令,靠方法表中的ACC_SYNCHRONIZED标志在调用时隐式加锁。


十五、注解与模块(简述)

  • 注解:编译进 class 文件(见规范 §4.7.16+);包级注解会编译成一个叫package-name.package-info的接口类文件。
  • 模块:模块声明编译成含Module属性的module-info.class(设ACC_MODULE标志)。该属性列出requires/exports/opens/uses/provides;除java.base外,模块必须显式声明依赖java.base(没写编译器会自动补一个ACC_MANDATED项)。

小结

  • 栈架构:运算在操作数栈上完成,iconst/bipush/ldc等把常量送进去
  • 控制流for/while都编译成「底部测试 +goto跳过首轮」;switch按密集度选tableswitch/lookupswitch
  • 方法调用invokevirtual(虚)/invokestatic(静态)/invokespecial(父类/构造)
  • 对象new+dup+invokespecial <init>是固定套路;字段用getfield/putfield
  • 数组newarray/anewarray/multianewarray
  • 异常throw=new+dup+<init>+athrowcatch靠异常表
  • 同步monitorenter/monitorexit+ 异常表兜底,方法级用ACC_SYNCHRONIZED
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/25 14:55:25

Agent Content Protocol: 一种基于 RFC 2046 的内容封装规范

Agent Content Protocol: 一种基于 RFC 2046 的内容封装规范 2026-08-22 Agent Content Protocol: 一种基于 RFC 2046 的内容封装规范 版本&#xff1a;0.5.0-draft1. 协议定位与核心宣言 1.1 定位 ACP 定义了一种可互操作的 Agent 消息内容信封。它规定了如何将一个 JSON 控制…

作者头像 李华
网站建设 2026/8/25 14:51:44

SkillGo,支持多用户与独立沙箱执行的 Skill 平台

一、为什么要做 SkillGo&#xff1f; 聚焦于解决&#xff1a; 创造一个私有化部署的社区&#xff0c;Skill 获得以后&#xff0c;在社区中怎样管理、怎样运行、怎样隔离&#xff0c;以及怎样将skill配置成可以接入业务系统的api。 这是 SkillGo 出现的原因。 GitHub&#xff1…

作者头像 李华
网站建设 2026/8/25 14:49:11

Python3.10自然语言处理项目:HuggingFace+镜像部署教程

.10自然语言处理项目&#xff1a;镜像部署教程想要迅速搭建一个归属于自身的 AI 开发环境, 然而又不愿被繁杂的依赖以及版本冲突弄得顾此失彼、疲惫不堪? 今日, 就在这里讲述一下怎样运用最为简单的方式, 在.10 环境里面, 借由一个预先配置好的镜像, 顺利实行部署生态, 进而开…

作者头像 李华