Java字节码扩展
异常表
代码一:
public class Test03 {
public void test() {
try {
InputStream is = new FileInputStream("123.txt");
ServerSocket serverSocket = new ServerSocket(1234);
serverSocket.accept();
} catch (FileNotFoundException f) {
} catch (IOException i) {
} catch (Exception e) {
} finally {
System.out.println("finally");
}
}
}
编译代码一,然后利用javap进行反编译查看结果:
...
Code:
stack=3, locals=4, args_size=1
0: new #2 // class java/io/FileInputStream
3: dup
4: ldc #3 // String 123.txt
6: invokespecial #4 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #5 // class java/net/ServerSocket
13: dup
14: sipush 1234
17: invokespecial #6 // Method java/net/ServerSocket."<init>":(I)V
20: astore_2
21: aload_2
22: invokevirtual #7 // Method java/net/ServerSocket.accept:()Ljava/net/Socket;
25: pop
26: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
29: ldc #9 // String finally
31: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
34: goto 84
37: astore_1
38: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
41: ldc #9 // String finally
43: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: goto 84
49: astore_1
50: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
53: ldc #9 // String finally
55: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
58: goto 84
61: astore_1
62: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
65: ldc #9 // String finally
67: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
70: goto 84
73: astore_3
74: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
77: ldc #9 // String finally
79: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
82: aload_3
83: athrow
84: return
Exception table:
from to target type
0 26 37 Class java/io/FileNotFoundException
0 26 49 Class java/io/IOException
0 26 61 Class java/lang/Exception
0 26 73 any
...
因为我们讨论是异常表,所以我只贴处test方法的code attribute的部分内容。
首先看第一行:stack=3, locals=4, args_size=1
,stack=3
表示test方法运行的任何时刻所能达到的操作数栈的最大深度为3,locals=4
表示test方法执行期间创建的局部变量的数目为4,args_size=1
表示test方法的参数个数为1。这时我们过来看代码,局部变量表的个数:catch块中,首先InputStream
,然后ServerSocket
,共有2个局部变量;catch块中会产生一个局部变量;finally中没有局部变量;那么也就只有3个局部变量,而不是4个。而且,test方法没有参数,为什么 args_size的值为1。
这里需要引入一个知识点:this关键字
对于Java中的每一个实例方法(非static方法),其在编译后所生成的字节码中, 方法参数的数量总是比源代码中方法参数的数量多一个(this),它位于方法的第一个参数。这样,我们就可以在Java的实例方法中使用this来去访问当前对象的属性以及其他方法。这个操作是在编译期间完成的,即由javac编译器在编译的时候将对this的访问转换为对一个普通实例方法参数的访问。接下来在运行期间,由JVM调用实例方法时,自动向实例方法传入该this参数。所以,在实例方法的局部变量中,至少会有一个指向当前对象的局部变量。
接下来看异常表:
Exception table:
from to target type
0 26 37 Class java/io/FileNotFoundException
0 26 49 Class java/io/IOException
0 26 61 Class java/lang/Exception
0 26 73 any
异常表中有4个值,每个值的start_pc都为0,end_pc都为26,这是因为catch块catch的都是try块中的内容;handler_pc(处理异常的代码的开始处)分别为37、49、61、73,我们可以发现每段异常的方法都是相同的(因为catch本身就没有写处理方法,直接进入finally块),我们可以发现;catch_type分别是FileNotFoundException、IOException、Exception、any,前三个是catch块catch的异常,any表示处理任何异常。
Java字节码对于异常的处理方式:
- 统一采用异常表的方式来对异常进行处理。
- 在jdk1.4.2之前的版本中,并不是使用异常表的方式来对异常进行处理,而是采用特定的指令。
- 当异常处理存在finally语句块时,现代化的JVM采用的处理方式是将finally语句块的字节码拼接到每一个catch块的后面。换句话说,程序中存在多少个catch块,就会有在每个catch块后面重复多少个finally语句块的字节码。
栈帧与操作数栈
- 栈帧(stack frame):栈帧是一种用于帮助虚拟机执行方法调用及方法执行的数据结构。
- 栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
符号引用与直接引用
比如现有两个类:A类和B类,A类存在对B类的方法调用,在编译期间,A对B的方法调用,它们之间的地址关系是不知道的。
什么时候才能知道呢?
- 类加载的时候
- 真正开始调用的时候
符号引用:A类去调用B类,在A的常量池中,会维护B的全局唯一限定名
直接引用:直接在内存中寻找到被调用的方法
-
有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫做静态解析
-
静态解析的四种情形,以下四类方法称作非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用
-
静态方法
-
父类方法
-
构造方法
-
私有方法
-
-
-
另外一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,这体现为Java的多态性
方法的静态分派和动态分派
方法的静态分派
Grandpa g1 = new Father();
g1的静态类型是Grandpa,该g1的实际类型(真正指向的类型)是Father
结论:变量的静态类型是不会发生变化,而变量的实际类型则是可以发生变化的(多态的一种体现),实际类型在运行期方可确定。
方法的动态分派:
nvokevirtual字节码指令的动态查找流程
- 寻找操作数栈的栈顶的元素所指向的对象的实际类型
- 如果在实际类型在当中寻找到了与常量池中描叙符和名称都相同的方法,并且具备相应的访问权限,返回目标方法的直接引用
- 如果找不到,就按照继承的层次关系从子类往父类依次重复查找流程;如果一直找不到,则抛出异常
通过比较方法重载和方法重写,得到这样一个结论:
- 方法重载是静态的,是编译期行为
- 方法重写是动态的,是运行期行为
基于栈的指令集于基于寄存器的指令集
现代JVM在执行Java代码的时候,通常会将解释执行和编译执行二者结合起来执行。
- 解释执行:通过解释器来读取字节码,遇到相应的指令就去执行该指令。
- 编译执行:通过即时编译器(JIT:Just In Time)将字节码转换为本地机器码来执行;现代JVM会根据代码热点来生成相应的本地机器码
基于栈的指令集与基于寄存器的指令集之间的关系:
- JVM执行指令时所采取的方式基于栈的指令集
- 基于栈的指令集主要的操作有入栈和出栈两种
- 基于栈的指令集的优势在于它可以在不同平台之间移植;而基于寄存器的指令集是与硬件架构紧密相关的,无法做到可移植
- 基于栈的指令集缺点在于完全相同的操作,指令数量通常要比基于寄存器的指令集数量要多 基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它是在高速缓冲区中进行执行的,速度要快很多 虽然虚拟机可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些