关于华大18年专注seo服务网站制作应用开发,wordpress文章页获取标签代码,网站建设职责,wordpress 链接数据库Java字节码技术
Java字节码是java代码编译后的中间代码格式#xff0c;JVM需要读取并解析字节码才能执行相应的任务 获取字节码简介#xff1a;由单字节(byte)的指令组成 操作码#xff08; 指令#xff09;, 主要由类型前缀和操作名称两部分组成。根据指令的性质#xf…Java字节码技术
Java字节码是java代码编译后的中间代码格式JVM需要读取并解析字节码才能执行相应的任务 获取字节码简介由单字节(byte)的指令组成 操作码 指令, 主要由类型前缀和操作名称两部分组成。根据指令的性质主要分为四个大类 栈操作指令包括与局部变量交互的指令程序流程控制指令对象操作指令包括方法调用指令算术运算以及类型转换指令 获取字节码清单 用 javap 工具来获取 class 文件中的指令清单专门用于反编译 class 文件。 Compiled from HelloByteCode.java
public class demo.jvm0104.HelloByteCode {public demo.jvm0104.HelloByteCode();Code:0: aload_01: invokespecial #1 // Method java/lang/Object.init:()V4: returnpublic static void main(java.lang.String[]);Code:0: new #2 // class demo/jvm0104/HelloByteCode3: dup4: invokespecial #3 // Method init:()V7: astore_18: return
} 解读字节码清单 public demo.jvm0104.HelloByteCode(); // 如果不定义任何构造函数就会有一个默认的无参构造函数.这是 Java 编译器生成的 而不是运行时JVM自动生成的。//每个构造函数中会先调用super类的构造函数默认构造函数中有些字节码指令来干这个事情
//解析的java/lang/Object 默认继承了Object类
public demo.jvm0104.HelloByteCode();Code:0: aload_01: invokespecial #1 // Method java/lang/Object.init:()V4: return查看class中的常量池 常量池就是一个常量的大字典使用编号的方式把程序里用到的各类常量统一管理起来这样在字节码操作里只需要引用编号即可。大多数时候指的是 运行时常量池。运行时常量池里面的常量主要是由 class 文件中的 常量池结构体 组成的。查看常量池信息的命令javap -c -verbose demo.jvm0104.HelloByteCode 反编译class的时候指定-verbose选项会输出附加信息 #1 Methodref #4.#13 // java/lang/Object.init:()V,#1常量编号该文件中其他地方可以引用 分隔符Methodref表明这个常量指向的是一个方法具体是哪个类的哪个方法呢? 类指向的 #4, 方法签名指向的 #13; 查看方法信息 //main方法编译结果public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack2, locals2, args_size1 方法描述: ([Ljava/lang/String;)V小括号内是入参信息/形参信息左方括号表述数组L 表示对象后面的java/lang/String就是类名称小括号后面的 V 则表示这个方法的返回值是 void方法的访问标志也很容易理解 flags: ACC_PUBLIC, ACC_STATIC表示 public 和 static。 还可以看到执行该方法时需要的栈(stack)深度是多少需要在局部变量表中保留多少个槽位, 还有方法的参数个数: stack2, locals2, args_size1。 线程栈与字节码执行模型 每个线程都有一个独属于自己的线程栈(JVM stack)用于存储栈帧(Frame)。每一次方法调用JVM都会自动创建一个栈帧。 栈帧 由 操作数栈 局部变量数组 以及一个class 引用组成。class 引用 指向当前方法在运行时常量池中对应的 class)。 局部变量数组 也称为 局部变量表(LocalVariableTable), 其中包含了方法的参数以及局部变量。 局部变量数组的大小在编译时就已经确定: 和局部变量形参的个数有关还要看每个变量/参数占用多少个字节。操作数栈是一个 LIFO 结构的栈 用于压入和弹出值。 它的大小也在编译时确定。有一些操作码/指令可以将值压入“操作数栈” 还有一些操作码/指令则是从栈中获取操作数并进行处理再将结果压入栈。操作数栈还用于接收调用其他方法时返回的结果值。 方法体中的字节码解读 0: new #2 // class demo/jvm0104/HelloByteCode3: dup4: invokespecial #3 // Method init:()V7: astore_18: return前面的数字间隔不相等的原因是, 有一部分操作码会附带有操作数, 也会占用字节码数组中的空间。 例如new 就会占用三个槽位: 一个用于存放操作码指令自身两个用于存放操作数。因此下一条指令 dup 的索引从 3 开始。 对象初始化指令new 指令, init 以及 clinit 简介 创建类实例生成操作码 0: new #2 // class demo/jvm0104/HelloByteCode 创建对象但没有调用构造函数
3: dup // 用来调用某些特殊方法的即构造函数
4: invokespecial #3 // Method init:()V 用于复制栈顶的值。构造函数调用不会返回值所以如果没有 dup 指令, 在对象上调用方法并初始化之后操作数栈就会是空的在初始化之后就会出问题。所以在构造函数返回之后可以将对象实例赋值给局部变量或某个字段接下来指令 astore {N} or astore_{N} – 赋值给局部变量其中 {N} 是局部变量表中的位置。
putfield – 将值赋给实例字段
putstatic – 将值赋给静态字段在调用构造函数的时候还会执行另一个类似的方法 init 甚至在执行构造函数之前就执行了。 还有一个可能执行的方法是该类的静态初始化方法 clinit 但 clinit 并不能被直接调用而是由这些指令触发的 new, getstatic, putstatic or invokestatic。 栈内存操作指令 最基础的是 dup 和 pop 指令。 dup 指令复制栈顶元素的值。pop 指令则从栈中删除最顶部的值。 复杂一点的指令比如swap, dup_x1 和 dup2_x1。 swap 指令可交换栈顶两个元素的值例如A和B交换位置(图中示例4)dup_x1 将复制栈顶元素的值并在栈顶插入两次(图中示例5)dup2_x1 则复制栈顶两个元素的值并插入第三个值(图中示例6)。 dup 指令复制栈顶的值并将复制的值压入栈。dup_x1 指令复制栈顶的值并将复制的值插入到最上面 2 个值的下方。dup2_x1 指令复制栈顶 1 个 64 位/或 2 个 32 位的值, 并将复制的值按照原始顺序插入原始值下面一个 32 位值的下方。 局部变量表 stack 主要用于执行指令而局部变量则用来保存中间结果两者之间可以直接交互。 javac -g demo/jvm0104/*.java(生成调试信息的 -g 参数) javap -c -verbose demo/jvm0104/LocalVariableTest (反编译) 代码 //移动平均数
public class MovingAverage {private int count 0;private double sum 0.0D;public void submit(double value){this.count ;this.sum value;}public double getAvg(){if(0 this.count){ return sum;}return this.sum/this.count;}
}public class LocalVariableTest {public static void main(String[] args) {MovingAverage ma new MovingAverage();int num1 1;int num2 2;ma.submit(num1);ma.submit(num2);double avg ma.getAvg();}
}反编译
public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack3, locals6, args_size10: new #2 // class demo/jvm0104/MovingAverage new, 创建 MovingAverage 类的对象;3: dup // 复制栈顶引用值。4: invokespecial #3 // Method demo/jvm0104/MovingAverage.init:()V invokespecial 执行对象初始化。7: astore_1 //使用 astore_1 指令将引用地址值(addr.)存储(store)到编号为1的局部变量中 astore_1 中的 1 指代 LocalVariableTable 中ma对应的槽位编号8: iconst_1 // iconst_1 和 iconst_2 用来将常量值1和2加载到栈里面 并分别由指令 istore_2 和 istore_3 将它们存储到在 LocalVariableTable 的槽位 2 和槽位 3 中。store 之类的指令调用实际上从栈顶删除了一个值。 这就是为什么再次使用相同值时必须再加载(load)一次的原因。9: istore_210: iconst_211: istore_312: aload_113: iload_214: i2d15: invokevirtual #4 // Method demo/jvm0104/MovingAverage.submit:(D)V18: aload_119: iload_320: i2d21: invokevirtual #4 // Method demo/jvm0104/MovingAverage.submit:(D)V24: aload_1 //调用 getAvg() 方法后返回的结果位于栈顶然后使用 dstore 将 double 值保存到本地变量4号槽位这里的d表示目标变量的类型为double。25: invokevirtual #5 // Method demo/jvm0104/MovingAverage.getAvg:()D28: dstore 430: returnLineNumberTable:line 5: 0line 6: 8line 7: 10line 8: 12line 9: 18line 10: 24line 11: 30LocalVariableTable:Start Length Slot Name Signature0 31 0 args [Ljava/lang/String;8 23 1 ma Ldemo/jvm0104/MovingAverage;10 21 2 num1 I12 19 3 num2 I30 1 4 avg D给局部变量赋值时需要使用相应的指令来进行 store如 astore_1。store 类的指令都会删除栈顶值。 相应的 load 指令则会将值从局部变量表压入操作数栈但并不会删除局部变量中的值。 流程控制指令 主要是分支和循环在用, 根据检查条件来控制程序的执行流程。 代码 public class ForLoopTest {private static int[] numbers {1, 6, 8};public static void main(String[] args) {MovingAverage ma new MovingAverage();for (int number : numbers) {ma.submit(number);}double avg ma.getAvg();}
}编译反编译 javac -g demo/jvm0104/*.java
javap -c -verbose demo/jvm0104/ForLoopTest字节码 0: new #2 // class demo/jvm0104/MovingAverage
3: dup
4: invokespecial #3 // Method demo/jvm0104/MovingAverage.init:()V
7: astore_1
8: getstatic #4 // Field numbers:[I
11: astore_2
12: aload_2
13: arraylength
14: istore_3
15: iconst_0
16: istore 418: iload 4 //循环体 用于执行循环计数器与数组长度的比较20: iload_321: if_icmpge 43 //if, integer, compare, great equal, 如果一个数的值大于或等于另一个值则程序执行流程跳转到pc43的地方继续执行。24: aload_225: iload 427: iaload28: istore 530: aload_131: iload 533: i2d34: invokevirtual #5 // Method demo/jvm0104/MovingAverage.submit:(D)V 37: iinc 4, 1 // 4号槽位的值加140: goto 18 //跳到循环开始的地方43: aload_144: invokevirtual #6 // Method demo/jvm0104/MovingAverage.getAvg:()D47: dstore_248: returnLocalVariableTable:Start Length Slot Name Signature30 7 5 number I //5 号槽位被 number 占用了。0 49 0 args [Ljava/lang/String; //0槽位被 main 方法的参数 args 占据了8 41 1 ma Ldemo/jvm0104/MovingAverage; //1 号槽位被 ma 占用了。48 1 2 avg D //2 号槽位是for循环之后才被 avg 占用的。2号槽位的变量保存了 numbers 的引用值占据了 2号槽位。
3号槽位的变量, 由 arraylength 指令使用, 得出循环的长度。
4号槽位的变量, 是循环计数器 每次迭代后使用 iinc 指令来递增。 算术运算指令与类型转换指令 将 int 值作为参数传递给实际上接收 double 的 submit() 方法时 在实际调用该方法之前使用了类型转换的操作码 31: iload 533: i2d34: invokevirtual #5 // Method demo/jvm0104/MovingAverage.submit:(D)V将一个 int 类型局部变量的值, 作为整数加载到栈中然后用 i2d 指令将其转换为 double 值以便将其作为参数传给submit方法。 唯一不需要将数值load到操作数栈的指令是 iinc它可以直接对 LocalVariableTable 中的值进行运算。 其他的所有操作均使用栈来执行。 方法调用指令和参数传递 用于方法调用的指令 invokestatic用于调用某个类的静态方法这也是方法调用指令中最快的一个。 invokespecial, 用来调用构造函数也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。 invokevirtual如果是具体类型的目标对象用于调用公共受保护和打包私有方法。 invokeinterface调用的方法属于某个接口。运行时受到更多限制 区别 使用 invokestatic 指令JVM 就确切地知道要调用的是哪个方法因为调用的是静态方法只能属于一个类。 使用 invokespecial 时 查找的数量也很少 解析也更加容易 那么运行时就能更快地找到所需的方法。 JDK7 新增的方法调用指令 invokedynamic 是实现“动态类型语言”在不改变字节码的时候,Java 语言层面想调用一个类 A 的方法 m只有两个办法 使用A anew A(); a.m()拿到一个 A 类型的实例然后直接调用方法通过反射通过 A.class.getMethod 拿到一个 Method然后再调用这个Method.invoke反射调用 invokedynamic配合新增的方法句柄Method Handles可以用来描述一个跟类型 A 无关的方法 m 的签名甚至不包括方法名称这样就可以做到我们使用方法 m 的签名但是直接执行的时候调用的是相同签名的另一个方法 b可以在运行时再决定由哪个类来接收被调用的方法。在此之前只能使用反射来实现类似的功能。该指令使得可以出现基于 JVM 的动态语言让 jvm 更加强大。而且在 JVM 上实现动态调用机制不会破坏原有的调用机制。这样既很好的支持了 Scala、Clojure 这些 JVM 上的动态语言又可以支持代码里的动态 lambda 表达式。