ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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需要javac和javap知道「栈后进先出」是什么一、先学会看「虚拟机汇编」示例都是「Java 源码 编译后的字节码清单」成对出现。字节码用javap输出的「虚拟机汇编语言」表示每行格式是偏移 操作码 [ 操作数1 [ 操作数2... ]] [// 注释]例如8 bipush 100 // Push int constant 1008这条指令在方法字节码数组里的偏移量bipush**操作码opcode**助记符100操作数后面是注释运行时常量池的引用用#前缀10 ldc #1 // 从常量池 #1 推入 float 常量 9 invokevirtual #4 // 调用 Example.addTwo(II)I后面所有示例都遵循这个格式。先看偏移、再看 opcode、最后看注释很快就能形成直觉。二、常量、局部变量与控制结构JVM 是「栈机器」最重要的心智模型JVM 是基于栈的。大多数操作从「当前帧的操作数栈」弹出操作数、算完再把结果压回去。方法被调用时会新建一个帧自带一个操作数栈和一组局部变量。2.1 一个for循环在字节码里长什么样voidspin(){inti;for(i0;i100;i){;// 循环体为空}}编译为0 iconst_0 // 把 int 常量 0 压栈 1 istore_1 // 存到局部变量 1i 0 2 goto 8 // 第一次先跳过自增 5 iinc 1 1 // 局部变量 1 自增 1i 8 iload_1 // 把局部变量 1 压栈i 9 bipush 100 // 把 int 常量 100 压栈 11 if_icmplt 5 // 若 i 100跳回 5 继续 14 return // 否则返回观察几个细节常量0用iconst_0带「隐式操作数」的专用指令100用bipush单字节立即数i是int存在局部变量槽 1istore_1/iload_1循环条件i 100被编译成「压栈 i → 压栈 100 →if_icmplt比较跳转」2.2double循环没有dincvoiddspin(){doublei;for(i0.0;i100.0;i){;}}关键片段5 dload_1 6 dconst_1 7 dadd // 用 dadd 做自增因为【没有 dinc 指令】 8 dstore_1 ... 13 dcmpg // 浮点比较用 dcmpg 14 iflt 5 // 配合 iflt没有 if_dcmpltdouble占两个局部变量槽自增不能像int那样iinc只能用dloaddconst_1dadddstore浮点比较没有「带条件的比较跳转」而是dcmpg先比较、再配合iflt这类通用跳转2.3short循环i2s截断voidsspin(){shorti;for(i0;i100;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((igrain-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_m1ixor实现的——这是编译器常用技巧。四、访问运行时常量池小常量走「快车道」把不同大小的常量推入栈JVM 给了不同的指令常量类型指令小int-1、0、1、2、3、4、5iconst_i如iconst_0单字节intbipush两字节intsipush大int/float/Stringldc/ldc_w从常量池取long/doublelconst_l/dconst_d、ldc2_w示例voiduseManyNumeric(){inti100;intj1000000;longl11;longl20xffffffff;// 即 int 的 -1doubled2.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(){inti0;while(i100){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 returnwhile和for编译出来几乎一样——测试在底部goto先跳过首轮自增。六、接收参数this占的槽位很重要的一条规则实例方法参数从局部变量槽1开始槽0永远是this静态方法没有this参数从槽0开始intaddTwo(inti,intj){returnij;}// 实例方法staticintaddTwoStatic(inti,intj){returnij;}// 静态方法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静态方法不传thisinvokespecial父类方法、构造器init跳过虚分派invokeinterface接口方法本例未展开调用实例方法注意要先aload_0把this压栈intadd12and13(){returnaddTwo(12,13);}0 aload_0 // 压入 this 1 bipush 12 3 bipush 13 5 invokevirtual #4 // Method Example.addTwo(II)I 8 ireturn调用父类方法用invokespecialclassFarextendsNear{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){ivalue;}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/multianewarrayvoidcreateBuffer(){intbuffer[];intbufsz100;intvalue12;buffernewint[bufsz];buffer[10]value;valuebuffer[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多维数组用multianewarrayint[][][]create3DArray(){intgrid[][][];gridnewint[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稀疏用lookupswitchswitch只支持int其它类型编译期会被提升为int。密集连续的 case →tableswitchintchooseNear(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;}privatelongindex0;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 // 返回旧值十二、异常throw与try-catchthrow new X()编译成「newdupinvokespecial initathrow」voidcantBeZero(inti)throwsTestExc{if(i0){thrownewTestExc();}}0 iload_1 1 ifne 12 // 若 i ! 0 跳过 4 new #1 // new TestExc 7 dup 8 invokespecial #7 // init 11 athrow // 抛出 12 returntry-catch不靠特殊跳转而是靠异常表Exception tablevoidcatchOne(){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/monitorexitsynchronized块的编译进入时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父类/构造对象newdupinvokespecial init是固定套路字段用getfield/putfield数组newarray/anewarray/multianewarray异常thrownewdupinitathrowcatch靠异常表同步monitorenter/monitorexit 异常表兜底方法级用ACC_SYNCHRONIZED
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进