接上文:Java中的类文件结构之二:分析一个.class文件的文本化阅读
https://blog.youkuaiyun.com/kcstrong/article/details/81233672
在上一篇Blog中找一个示例讲述了如何分析JVM所提供的文本形态说明的.class文件,其中各方法的字节码部分(表述该方法的实现)并没有说清楚,本章中希望说清楚各逻辑,但考虑到如果不讲一下字节码的详细语法,即使分析清楚了上一篇中的示例,也是管中窥豹,只见一斑。因而在分析示例前,先说一下字节码的详细语法,若希望直接看示例的字节码含义部分,可以直接看下一篇的讲述。
Java的字节码语法相对于Java语言,类假于汇编之对于C/C++语言。一般来讲,Java语言的开发者是不需要了解的,不了解字节码,也能写出Java语法,但不一定能百分之百用好Java语言。在工程上,尤其是当前互联网行业竞争相当激烈的环境,有时候九十分是不令人满意,一百分才行,做为有追求的Java程序猿,我们需要熟练的掌握字节码语法,这样才能在系统调优,疑难问题排查方面做好。
以下从几个方面来讲一下Java的字节码:
第一部分讲一下字节码都有什么,怎么样分的类别
第二部分讲一下栈结构的一些基本概念,涉及虚拟机栈、线程、栈桢、操作数栈、局部变量表等
第三部分讲一下class二进制文件怎么得到文本形态的字节符描述
第四部分讲一下由Java类编译出的class中方法的字节码如何工作,以示例说明
一、字节码列表如下所示:
字节码 | 助记符 | 指令含义 |
0x00 | nop | None |
0x01 | aconst_null | 将null推送至栈顶 |
0x02 | iconst_m1 | 将int型-1推送至栈顶 |
0x03 | iconst_0 | 将int型0推送至栈顶 |
0x04 | iconst_1 | 将int型1推送至栈顶 |
0x05 | iconst_2 | 将int型2推送至栈顶 |
0x06 | iconst_3 | 将int型3推送至栈顶 |
0x07 | iconst_4 | 将int型4推送至栈顶 |
0x08 | iconst_5 | 将int型5推送至栈顶 |
0x09 | lconst_0 | 将long型0推送至栈顶 |
0x0a | lconst_1 | 将long型1推送至栈顶 |
0x0b | fconst_0 | 将float型0推送至栈顶 |
0x0c | fconst_1 | 将float型1推送至栈顶 |
0x0d | fconst_2 | 将float型2推送至栈顶 |
0x0e | dconst_0 | 将double型0推送至栈顶 |
0x0f | dconst_1 | 将double型1推送至栈顶 |
0x10 | bipush | 将单字节的常量值(-128~127)推送至栈顶 |
0x11 | sipush | 将一个短整型常量(-32768~32767)推送至栈顶 |
0x12 | ldc | 将int,float或String型常量值从常量池中推送至栈顶 |
0x13 | ldc_w | 将int,float或String型常量值从常量池中推送至栈顶(宽索引) |
0x14 | ldc2_w | 将long或double型常量值从常量池中推送至栈顶(宽索引) |
0x15 | iload | 将指定的int型本地变量推送至栈顶 |
0x16 | lload | 将指定的long型本地变量推送至栈顶 |
0x17 | fload | 将指定的float型本地变量推送至栈顶 |
0x18 | dload | 将指定的double型本地变量推送至栈顶 |
0x19 | aload | 将指定的引用类型本地变量推送至栈顶 |
0x1a | iload_0 | 将第一个int型本地变量推送至栈顶 |
0x1b | iload_1 | 将第二个int型本地变量推送至栈顶 |
0x1c | iload_2 | 将第三个int型本地变量推送至栈顶 |
0x1d | iload_3 | 将第四个int型本地变量推送至栈顶 |
0x1e | lload_0 | 将第一个long型本地变量推送至栈顶 |
0x1f | lload_1 | 将第二个long型本地变量推送至栈顶 |
0x20 | lload_2 | 将第三个long型本地变量推送至栈顶 |
0x21 | lload_3 | 将第四个long型本地变量推送至栈顶 |
0x22 | fload_0 | 将第一个float型本地变量推送至栈顶 |
0x23 | fload_1 | 将第二个float型本地变量推送至栈顶 |
0x24 | fload_2 | 将第三个float型本地变量推送至栈顶 |
0x25 | fload_3 | 将第四个float型本地变量推送至栈顶 |
0x26 | dload_0 | 将第一个double型本地变量推送至栈顶 |
0x27 | dload_1 | 将第二个double型本地变量推送至栈顶 |
0x28 | dload_2 | 将第三个double型本地变量推送至栈顶 |
0x29 | dload_3 | 将第四个double型本地变量推送至栈顶 |
0x2a | aload_0 | 将第一个引用类型本地变量推送至栈顶 |
0x2b | aload_1 | 将第二个引用类型本地变量推送至栈顶 |
0x2c | aload_2 | 将第三个引用类型本地变量推送至栈顶 |
0x2d | aload_3 | 将第四个引用类型本地变量推送至栈顶 |
0x2e | iaload | 将int型数组指定索引的值推送至栈顶 |
0x2f | laload | 将long型数组指定索引的值推送至栈顶 |
0x30 | faload | 将float型数组指定索引的值推送至栈顶 |
0x31 | daload | 将double型数组指定索引的值推送至栈顶 |
0x32 | aaload | 将引用类型数组指定索引的值推送至栈顶 |
0x33 | baload | 将boolean或byte型数组指定索引的值推送至栈顶 |
0x34 | caload | 将char型数组指定索引的值推送至栈顶 |
0x35 | saload | 将short型数组指定索引的值推送至栈顶 |
0x36 | istore | 将栈顶int型数值存入指定本地变量 |
0x37 | lstore | 将栈顶long型数值存入指定本地变量 |
0x38 | fstore | 将栈顶float型数值存入指定本地变量 |
0x39 | dstore | 将栈顶double型数值存入指定本地变量 |
0x3a | astore | 将栈顶引用类型数值存入指定本地变量 |
0x3b | istore_0 | 将栈顶int型数值存入第一个本地变量 |
0x3c | istore_1 | 将栈顶int型数值存入第二个本地变量 |
0x3d | istore_2 | 将栈顶int型数值存入第三个本地变量 |
0x3e | istore_3 | 将栈顶int型数值存入第四个本地变量 |
0x3f | lstore_0 | 将栈顶long型数值存入第一个本地变量 |
0x40 | lstore_1 | 将栈顶long型数值存入第二个本地变量 |
0x41 | lstore_2 | 将栈顶long型数值存入第三个本地变量 |
0x42 | lstore_3 | 将栈顶long型数值存入第四个本地变量 |
0x43 | fstore_0 | 将栈顶float型数值存入第一个本地变量 |
0x44 | fstore_1 | 将栈顶float型数值存入第二个本地变量 |
0x45 | fstore_2 | 将栈顶float型数值存入第三个本地变量 |
0x46 | fstore_3 | 将栈顶float型数值存入第四个本地变量 |
0x47 | dstore_0 | 将栈顶double型数值存入第一个本地变量 |
0x48 | dstore_1 | 将栈顶double型数值存入第二个本地变量 |
0x49 | dstore_2 | 将栈顶double型数值存入第三个本地变量 |
0x4a | dstore_3 | 将栈顶double型数值存入第四个本地变量 |
0x4b | astore_0 | 将栈顶引用型数值存入第一个本地变量 |
0x4c | astore_1 | 将栈顶引用型数值存入第二个本地变量 |
0x4d | astore_2 | 将栈顶引用型数值存入第三个本地变量 |
0x4e | astore_3 | 将栈顶引用型数值存入第四个本地变量 |
0x4f | iastore | 将栈顶int型数值存入指定数组的指定索引位置 |
0x50 | lastore | 将栈顶long型数值存入指定数组的指定索引位置 |
0x51 | fastore | 将栈顶float型数值存入指定数组的指定索引位置 |
0x52 | dastore | 将栈顶double型数值存入指定数组的指定索引位置 |
0x53 | aastore | 将栈顶引用型数值存入指定数组的指定索引位置 |
0x54 | bastore | 将栈顶boolean或byte型数值存入指定数组的指定索引位置 |
0x55 | castore | 将栈顶char型数值存入指定数组的指定索引位置 |
0x56 | sastore | 将栈顶short型数值存入指定数组的指定索引位置 |
0x57 | pop | 将栈顶数值弹出(数值不能是long或double类型的) |
0x58 | pop2 | 将栈顶的一个(对于非long或double类型)或两个数值(对于非long或double的其他类型)弹出 |
0x59 | dup | 复制栈顶数值并将复制值压入栈顶 |
0x5a | dup_x1 | 复制栈顶数值并将两个复制值压入栈顶 |
0x5b | dup_x2 | 复制栈顶数值并将三个(或两个)复制值压入栈顶 |
0x5c | dup2 | 复制栈顶一个(对于long或double类型)或两个(对于非long或double的其他类型)数值并将复制值压入栈顶 |
0x5d | dup2_x1 | dup_x1指令的双倍版本 |
0x5e | dup2_x2 | dup_x2指令的双倍版本 |
0x5f | swap | 将栈顶最顶端的两个数值互换(数值不能是long或double类型) |
0x60 | iadd | 将栈顶两int型数值相加并将结果压入栈顶 |
0x61 | ladd | 将栈顶两long型数值相加并将结果压入栈顶 |
0x62 | fadd | 将栈顶两float型数值相加并将结果压入栈顶 |
0x63 | dadd | 将栈顶两double型数值相加并将结果压入栈顶 |
0x64 | isub | 将栈顶两int型数值相减并将结果压入栈顶 |
0x65 | lsub | 将栈顶两long型数值相减并将结果压入栈顶 |
0x66 | fsub | 将栈顶两float型数值相减并将结果压入栈顶 |
0x67 | dsub | 将栈顶两double型数值相减并将结果压入栈顶 |
0x68 | imul | 将栈顶两int型数值相乘并将结果压入栈顶 |
0x69 | lmul | 将栈顶两long型数值相乘并将结果压入栈顶 |
0x6a | fmul | 将栈顶两float型数值相乘并将结果压入栈顶 |
0x6b | dmul | 将栈顶两double型数值相乘并将结果压入栈顶 |
0x6c | idiv | 将栈顶两int型数值相除并将结果压入栈顶 |
0x6d | ldiv | 将栈顶两long型数值相除并将结果压入栈顶 |
0x6e | fdiv | 将栈顶两float型数值相除并将结果压入栈顶 |
0x6f | ddiv | 将栈顶两double型数值相除并将结果压入栈顶 |
0x70 | irem | 将栈顶两int型数值作取模运算并将结果压入栈顶 |
0x71 | lrem | 将栈顶两long型数值作取模运算并将结果压入栈顶 |
0x72 | frem | 将栈顶两float型数值作取模运算并将结果压入栈顶 |
0x73 | drem | 将栈顶两double型数值作取模运算并将结果压入栈顶 |
0x74 | ineg | 将栈顶int型数值取负并将结果压入栈顶 |
0x75 | lneg | 将栈顶long型数值取负并将结果压入栈顶 |
0x76 | fneg | 将栈顶float型数值取负并将结果压入栈顶 |
0x77 | dneg | 将栈顶double型数值取负并将结果压入栈顶 |
0x78 | ishl | 将int型数值左移指定位数并将结果压入栈顶 |
0x79 | lshl | 将long型数值左移指定位数并将结果压入栈顶 |
0x7a | ishr | 将int型数值右(带符号)移指定位数并将结果压入栈顶 |
0x7b | lshr | 将long型数值右(带符号)移指定位数并将结果压入栈顶 |
0x7c | iushr | 将int型数值右(无符号)移指定位数并将结果压入栈顶 |
0x7d | lushr | 将long型数值右(无符号)移指定位数并将结果压入栈顶 |
0x7e | iand | 将栈顶两int型数值"按位与"并将结果压入栈顶 |
0x7f | land | 将栈顶两long型数值"按位与"并将结果压入栈顶 |
0x80 | ior | 将栈顶两int型数值"按位或"并将结果压入栈顶 |
0x81 | lor | 将栈顶两long型数值"按位或"并将结果压入栈顶 |
0x82 | ixor | 将栈顶两int型数值"按位异或"并将结果压入栈顶 |
0x83 | lxor | 将栈顶两long型数值"按位异或"并将结果压入栈顶 |
0x84 | iinc | 将指定int型变量增加指定值(如i++, i--, i+=2等) |
0x85 | i2l | 将栈顶int型数值强制转换为long型数值并将结果压入栈顶 |
0x86 | i2f | 将栈顶int型数值强制转换为float型数值并将结果压入栈顶 |
0x87 | i2d | 将栈顶int型数值强制转换为double型数值并将结果压入栈顶 |
0x88 | l2i | 将栈顶long型数值强制转换为int型数值并将结果压入栈顶 |
0x89 | l2f | 将栈顶long型数值强制转换为float型数值并将结果压入栈顶 |
0x8a | l2d | 将栈顶long型数值强制转换为double型数值并将结果压入栈顶 |
0x8b | f2i | 将栈顶float型数值强制转换为int型数值并将结果压入栈顶 |
0x8c | f2l | 将栈顶float型数值强制转换为long型数值并将结果压入栈顶 |
0x8d | f2d | 将栈顶float型数值强制转换为double型数值并将结果压入栈顶 |
0x8e | d2i | 将栈顶double型数值强制转换为int型数值并将结果压入栈顶 |
0x8f | d2l | 将栈顶double型数值强制转换为long型数值并将结果压入栈顶 |
0x90 | d2f | 将栈顶double型数值强制转换为float型数值并将结果压入栈顶 |
0x91 | i2b | 将栈顶int型数值强制转换为byte型数值并将结果压入栈顶 |
0x92 | i2c | 将栈顶int型数值强制转换为char型数值并将结果压入栈顶 |
0x93 | i2s | 将栈顶int型数值强制转换为short型数值并将结果压入栈顶 |
0x94 | lcmp | 比较栈顶两long型数值大小, 并将结果(1, 0或-1)压入栈顶 |
0x95 | fcmpl | 比较栈顶两float型数值大小, 并将结果(1, 0或-1)压入栈顶; 当其中一个数值为NaN 时, 将-1压入栈顶 |
0x96 | fcmpg | 比较栈顶两float型数值大小, 并将结果(1, 0或-1)压入栈顶; 当其中一个数值为NaN 时, 将1压入栈顶 |
0x97 | dcmpl | 比较栈顶两double型数值大小, 并将结果(1, 0或-1)压入栈顶; 当其中一个数值为NaN 时, 将-1压入栈顶 |
0x98 | dcmpg | 比较栈顶两double型数值大小, 并将结果(1, 0或-1)压入栈顶; 当其中一个数值为NaN 时, 将1压入栈顶 |
0x99 | ifeq | 当栈顶int型数值等于0时跳转 |
0x9a | ifne | 当栈顶int型数值不等于0时跳转 |
0x9b | iflt | 当栈顶int型数值小于0时跳转 |
0x9c | ifge | 当栈顶int型数值大于等于0时跳转 |
0x9d | ifgt | 当栈顶int型数值大于0时跳转 |
0x9e | ifle | 当栈顶int型数值小于等于0时跳转 |
0x9f | if_icmpeq | 比较栈顶两int型数值大小, 当结果等于0时跳转 |
0xa0 | if_icmpne | 比较栈顶两int型数值大小, 当结果不等于0时跳转 |
0xa1 | if_icmplt | 比较栈顶两int型数值大小, 当结果小于0时跳转 |
0xa2 | if_icmpge | 比较栈顶两int型数值大小, 当结果大于等于0时跳转 |
0xa3 | if_icmpgt | 比较栈顶两int型数值大小, 当结果大于0时跳转 |
0xa4 | if_icmple | 比较栈顶两int型数值大小, 当结果小于等于0时跳转 |
0xa5 | if_acmpeq | 比较栈顶两引用型数值, 当结果相等时跳转 |
0xa6 | if_acmpne | 比较栈顶两引用型数值, 当结果不相等时跳转 |
0xa7 | goto | 无条件跳转 |
0xa8 | jsr | 跳转至指定的16位offset位置, 并将jsr的下一条指令地址压入栈顶 |
0xa9 | ret | 返回至本地变量指定的index的指令位置(一般与jsr或jsr_w联合使用) |
0xaa | tableswitch | 用于switch条件跳转, case值连续(可变长度指令) |
0xab | lookupswitch | 用于switch条件跳转, case值不连续(可变长度指令) |
0xac | ireturn | 从当前方法返回int |
0xad | lreturn | 从当前方法返回long |
0xae | freturn | 从当前方法返回float |
0xaf | dreturn | 从当前方法返回double |
0xb0 | areturn | 从当前方法返回对象引用 |
0xb1 | return | 从当前方法返回void |
0xb2 | getstatic | 获取指定类的静态域, 并将其压入栈顶 |
0xb3 | putstatic | 为指定类的静态域赋值 |
0xb4 | getfield | 获取指定类的实例域, 并将其压入栈顶 |
0xb5 | putfield | 为指定类的实例域赋值 |
0xb6 | invokevirtual | 调用实例方法 |
0xb7 | invokespecial | 调用超类构建方法, 实例初始化方法, 私有方法 |
0xb8 | invokestatic | 调用静态方法 |
0xb9 | invokeinterface | 调用接口方法 |
0xba | invokedynamic | 调用动态方法 |
0xbb | new | 创建一个对象, 并将其引用引用值压入栈顶 |
0xbc | newarray | 创建一个指定的原始类型(如int, float, char等)的数组, 并将其引用值压入栈顶 |
0xbd | anewarray | 创建一个引用型(如类, 接口, 数组)的数组, 并将其引用值压入栈顶 |
0xbe | arraylength | 获取数组的长度值并压入栈顶 |
0xbf | athrow | 将栈顶的异常抛出 |
0xc0 | checkcast | 检验类型转换, 检验未通过将抛出 ClassCastException |
0xc1 | instanceof | 检验对象是否是指定类的实际, 如果是将1压入栈顶, 否则将0压入栈顶 |
0xc2 | monitorenter | 获得对象的锁, 用于同步方法或同步块 |
0xc3 | monitorexit | 释放对象的锁, 用于同步方法或同步块 |
0xc4 | wide | 扩展本地变量的宽度 |
0xc5 | multianewarray | 创建指定类型和指定维度的多维数组(执行该指令时, 操作栈中必须包含各维度的长度值), 并将其引用压入栈顶 |
0xc6 | ifnull | 为null时跳转 |
0xc7 | ifnonnull | 不为null时跳转 |
0xc8 | goto_w | 无条件跳转(宽索引) |
0xc9 | jsr_w | 跳转至指定的32位offset位置, 并将jsr_w的下一条指令地址压入栈顶 |
字节码操作符后面会有一个或0个操作数,操作数可能的数据类型包括:l代表long,s代表short、i代表int、b代表byte、c代表char、f代表float、d代表double、a代表reference。以下提到了几个存储区:
局部变量表:在方法解析为字节码时就已定好的,存储所用到的局部变量的地方
操作数栈:一个先进后出的栈,用于执行字节码操作,入栈的可能是操作符,可能是操作数
常量:位于常量池中
便与记忆,可以将操作符分为以下几类:(源于《深入理解Java虚拟机》)
1.加载与存储指令
将一个数值从局部变量表中加载至操作数栈,如:iload(iload_0 == iload 0)
将一个数值从操作数栈存入局部变量表,如:istore(istore_0 == istore 0)
将一个常量加载到操作数栈,如:bipush
扩充局部变量表的访问索引,如:wide
2.运算指令
加减乘除,例:iadd、fdiv等
位运算,例:ior、ixor等
自增指令:iinc,注意,该运算符操作的是局部变量表,与栈无关
比较指令,例:lcmp
3.类型转换指令,例:i2b(整型int转byte)、f2i(float转int)
4.对象的创建与该问指令
创建类实例:new,注意:先创建的是类实例,然后才是调用构造方法,也就是说new A(),不止一条操作符
创建数组,例:newarray
访问类字段或实例字段:getfield、putfield、getstatic、putstatic
检查类型:instanceof、checkcast
其他数组操作
5.管理操作数栈:pop、pop2、dup等、swap
6.控制跳转:if有关的(ifeq等)、tableswitch、lookupswitch、goto等
7.方法调用:
invokevirual:调用对象的实例方法(除实现的接口方法外)
invokespecial:调用构造方法、私有方法、父类方法
invokestatic:调用类方法
invokeinterface:调用接口方法
invokedynamic:用户设定的引导方法(一些动态语言特性相关的支持,在JDK1.8引入)
8.异常处理指令:athrow
9.同步指令:monitorenter、monitorexit
二、相关的几个概念详述:
1.栈结构与寄存器结构:目前的PC指令集架构的两种方式,各有优缺点,栈结构平台相关性低,可以跨平台运行于不同的硬件系统上,在x86和arm系列可能同样处理。寄存器结构运行更快,运行相同逻辑所需要的操作数也要更少,比如Android虚拟机对资源相当敏感,就是寄存器的指令集结构。
2.虚拟机栈与线程、栈帧:Java的方法都是运行于线程中的,或者是主线程,或者是工作线程,每一个方法都是一个栈帧。多个方法(栈帧)组成了一个线程内存块、每个线程的内存放于一个虚拟机栈中,也就是我们常说的栈区(最大的对比区别是堆区),当前线程正在处理的就是位于最上方的栈帧,如下图所示(图片来源于:《深入理解Java虚拟机》):
3.操作数栈与局部变量表:均在方法编译为字节码的同时,就定义好了两者的大小,分别用:max_stack代表操作栈的最大深度,max_locals代表局部变量表的大小,注意,max_locals的单位为slot。JVM是基于栈的指令结构,各方法的运行就是通过操作栈与局部变量表配合实现的逻辑,在第一部分中可以看到从操作栈到局部变量表双向的操作。
三、看一下javap是如何由二进制文件得到文本描述的字节码表述的,看一个例子:
package com.demo.kcsdemo.java;
import java.util.Random;
public class Temp5Test {
int index;
public int incc(int incval) {
Random random = new Random();
int ri = random.nextInt();
index = index + incval;
return index;
}
}
编译该类:javac com/demo/kcsdemo/java/Temp5Test.java,二进制输出如下所示:
生成字节符描述:javap -verbose Temp5Test
Classfile /Users/wangguoqiang/AndroidStudioProjects/KCSDemo/app/src/main/java/com/demo/kcsdemo/java/Temp5Test.class
Last modified 2018-7-31; size 389 bytes
MD5 checksum 03fc35b2dc3f1aa80072f1a37baedd58
Compiled from "Temp5Test.java"
public class com.demo.kcsdemo.java.Temp5Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // java/util/Random
#3 = Methodref #2.#18 // java/util/Random."<init>":()V
#4 = Methodref #2.#20 // java/util/Random.nextInt:()I
#5 = Fieldref #6.#21 // com/demo/kcsdemo/java/Temp5Test.index:I
#6 = Class #22 // com/demo/kcsdemo/java/Temp5Test
#7 = Class #23 // java/lang/Object
#8 = Utf8 index
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 incc
#15 = Utf8 (I)I
#16 = Utf8 SourceFile
#17 = Utf8 Temp5Test.java
#18 = NameAndType #10:#11 // "<init>":()V
#19 = Utf8 java/util/Random
#20 = NameAndType #24:#25 // nextInt:()I
#21 = NameAndType #8:#9 // index:I
#22 = Utf8 com/demo/kcsdemo/java/Temp5Test
#23 = Utf8 java/lang/Object
#24 = Utf8 nextInt
#25 = Utf8 ()I
{
int index;
descriptor: I
flags:
public com.demo.kcsdemo.java.Temp5Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
public int incc(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: new #2 // class java/util/Random
3: dup
4: invokespecial #3 // Method java/util/Random."<init>":()V
7: astore_2
8: aload_2
9: invokevirtual #4 // Method java/util/Random.nextInt:()I
12: istore_3
13: aload_0
14: aload_0
15: getfield #5 // Field index:I
18: iload_1
19: iadd
20: putfield #5 // Field index:I
23: aload_0
24: getfield #5 // Field index:I
27: ireturn
LineNumberTable:
line 10: 0
line 11: 8
line 12: 13
line 13: 23
}
SourceFile: "Temp5Test.java"
直接看方法incc的部分:
public int incc(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: new #2 // class java/util/Random
3: dup
4: invokespecial #3 // Method java/util/Random."<init>":()V
7: astore_2
8: aload_2
9: invokevirtual #4 // Method java/util/Random.nextInt:()I
12: istore_3
13: aload_0
14: aload_0
15: getfield #5 // Field index:I
18: iload_1
19: iadd
20: putfield #5 // Field index:I
23: aload_0
24: getfield #5 // Field index:I
27: ireturn
LineNumberTable:
line 10: 0
line 11: 8
line 12: 13
line 13: 23
stack=3表示最大操作数栈深度为3,locals=4表示局部变,局部变量量表中有4个值,args_size=2表示有两个参数,为什么是两个参数呢?在实例方法(归属于对象的方法)中,第一个参数默认为this,即操作该方法的当前对象,然后按先后顺序排列其他的参数,因此incc方法有两个参数,局部变量表中第0位为this,然后为方法的参数表,由先到后,在incc中,第1位为参数incval。
该方法的二进制码位于如下图所示,在两个红色标识之间由00000120h0d~00000170h0a:
二进制的分析方法在第一篇:Java中的类文件结构之一:如何分析一个.class文件的二进制码内容
https://blog.youkuaiyun.com/kcstrong/article/details/79460262
之中有详细的描述,不明白的可以看一下。现在简单的再提练一下二进制位与字节码的对应关系:
0x0001:public方法,字节码的如下部分
flags: ACC_PUBLIC
0x000E000F:对应于常量池中的#14和#15,为该方法的名称和描述
#14 = Utf8 incc
#15 = Utf8 (I)I
字节码的如下部分:
public int incc(int);
descriptor: (I)I
0x0001:包含一个属性表,0x000C:对应于常量池中的#12,字节码部分:
Code:
0x00000040:属性表除名称和长度外还有0x40个字节
0x0003:max_stack为3,0x0004:max_local为4,对应于字节码表述为:
stack=3, locals=4,
0x0000001C:字节码长1*16+12=28位,后面的28位即为字节码:
0xBB00...05AC,再看一下字节码表述,正好28位(最左侧一排即为操作符的位偏移量):
0: new #2 // class java/util/Random
3: dup
4: invokespecial #3 // Method java/util/Random."<init>":()V
7: astore_2
8: aload_2
9: invokevirtual #4 // Method java/util/Random.nextInt:()I
12: istore_3
13: aload_0
14: aload_0
15: getfield #5 // Field index:I
18: iload_1
19: iadd
20: putfield #5 // Field index:I
23: aload_0
24: getfield #5 // Field index:I
27: ireturn
从里面找个例子说明下吧,比如,第15条操作符
15: getfield #5 // Field index:I
从0xBB开始,第15位偏移的二进字为0xB4,查找本篇一开始的操作符表,可知,getfield在表中的位置正是b4位
该操作符后面跟随着操作数,占两位(16、17),因为下一条操作是从18开始的,因而操作数为0x0005,正是常量池中的#5常量。
字节码后面的二进制对应直接从属性表一一查找,即可,不再展开了,下图为属性表的各字段含义:
四、看一下上述例子中的字节码的执行过程是什么:
0: new #2 // class java/util/Random
3: dup
4: invokespecial #3 // Method java/util/Random."<init>":()V
7: astore_2
8: aload_2
9: invokevirtual #4 // Method java/util/Random.nextInt:()I
12: istore_3
13: aload_0
14: aload_0
15: getfield #5 // Field index:I
18: iload_1
19: iadd
20: putfield #5 // Field index:I
23: aload_0
24: getfield #5 // Field index:I
27: ireturn
第0行new执行前的栈为空,局部变量表为如下所示:
索引 | 变量 | 类型 |
0 | this | 引用a |
1 | incval | 整型i |
2 | ||
3 |
第0~4条操作符为创建Random对象,通过new创建对象的过程均为该写法,调用了Random的类实例及构造方法,初始化为random变量,执行完毕后局部变量表不变,操作数栈如下:
random |
第7条操作,将操作数栈顶的变量存入局部变量表中,操作完毕后的局部变量表如下所示,栈为空
索引 | 变量 | 类型 |
0 | this | 引用a |
1 | incval | 整型i |
2 | random | 引用a |
3 |
第8条操作将局部变量表中的第2条变量入栈,执行完毕后操作数栈如下:
random |
第9条操作调用random的nextInt()方法,执行完毕后操作数栈如下所示:
ri |
第12条将栈顶元素入局部变量表第3的位置:
索引 | 变量 | 类型 |
0 | this | 引用a |
1 | incval | 整型i |
2 | random | 引用a |
3 | ri | 整型i |
第13、14条执行完毕后,栈变为:
this |
this |
第15条调用this的getfield,得到当前对象的相关域index并放入栈顶,栈变为:
index |
this |
第18条执行从局部变量表载入incval,栈变为:
incval |
index |
this |
第19条将栈顶元素出顶相加再入栈,栈变为:
index+incval的值 |
this |
第20条将值放入index域中,栈中元素均已出栈,栈空
第23、24条为读入this,然后再得到index域并放入栈顶,执行完毕后栈为:
index |
第27条返回该值
本文为作者原创作品,写作不易,转载请注明出处