动态类型语言支持
动态类型语言,就是类型检查的主体过程在运行期,而非编译期的编程语言。
动/静态类型语言各自的优点?
- 动态类型语言:灵活性高,开发效率高。
- 静态类型语言:编译器提供了严谨的类型检查,类型相关的问题能在编码的时候就发现。
动态类型语言
根据前面提到的动态类型语言的定义,我们将举两个例子来介绍一下什么是“类型检查”和什么是“在编译期还是运行期进行”。
1 2 3
| public static void main(String[] args){ int [][][] array = new int[1][0][-1]; }
|
上面这段Java代码能够正常编译,但运行的时候会出现NegativeArraySizeException
异常。
在《Java虚拟机规范》中明确规定了NegativeArraySizeException
是一个运行时异常(Runtime Exception
),通俗一点说,运行时异常就是指只要代码不执行到这一行就不会出现问题。
与运行时异常相对应的概念是连接时异常,例如很常见的NoClassDefFoundError
便属于连接时异常,即使导致连接时异常的代码放在一条根本无法被执行到的路径分支上,类加载时也照样会抛出异常。
不过,在C语言里,语义相同的代码就会在GCC编译期就直接报错,而不是等到运行时才出现异常。
由此看来,一门语言的哪一种检查行为要在运行期进行,哪一种检查要在编译期进行并没有什么必然的因果逻辑关系,关键是在语言规范中人为设立的约定。
Java虚拟机层面提供的动态类型支持:
Lambda
表达式就是通过 invokedynamic
指令实现的。
java.lang.invoke包
这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制,称为“方法句柄”(MethodHandle)。
方法句柄的使用
- 获得方法的参数描述,第一个参数是方法返回值的类型,之后的参数是方法的入参。
1 2 3 4 5 6 7 8 9 10 11 12
| MethodType mt = MethodType.methodType(void.class, String.class); ``` 2. 获取一个普通方法的调用 ```Java
MethodHandle.lookup().findVirtual(receiver.getClass(), "方法名", mt).bindTo(receiver);
|
- 获取一个父类方法的调用:
1 2 3 4 5 6 7 8
|
MethodHandle.lookup().findSpecial(GrandFather.class, "方法名", mt, getClass());
|
- 通过
MethodHandle mh
执行方法:
1 2 3 4 5 6 7
|
mh.invoke("Hello world"); mh.invokeExact("Hello world");
|
使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public class MethodHandleTest { static class ClassA { public void println(String s) { System.out.println(s); } }
public static void main(String[] args) throws Throwable {
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
getPrintlnMH(obj).invoke("Hello world"); getPrintlnMH(obj).invokeExact("Hello world"); }
private static MethodHandle getPrintlnMH(Object receiver) throws NoSuchMethodException, IllegalAccessException { MethodType mt = MethodType.methodType(void.class, String.class);
return MethodHandles.lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver); } }
|
MethodHandles.lookup
中 3 个方法对应的字节码指令:
findStatic()
:对应 invokestatic
findVirtual():
对应 invokevirtual
& invokeinterface
findSpecial():
对应 invokespecial
MethodHandle
和 Reflection
的区别
- 本质区别: 它们都在模拟方法调用,但是
Reflection
模拟的是 Java 代码层次的调用;
MethodHandle
模拟的是字节码层次的调用。
- 包含信息的区别:
Reflection
的 Method
对象包含的信息多,包括:方法签名、方法描述符、方法的各种属性的Java端表达方式、方法执行权限等;
MethodHandle
对象包含的信息比较少,既包含与执行该方法相关的信息。
实战演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Test{
class GrandFather{ void thinking(){ System.out.println("I am grandfather"); } }
class Father extends GrandFather{ void thinking(){ System.out.println("I am father"); } }
class Son extends Father{ void thinking(){ try{ MethodType mt = MethodType.methodType(void.class); MethodHandle mh = lookup().findSpecial(GrandFather.class,"thinking",mt,getClass()); mh.invoke(this); }catch(Throwable e){
} } }
public static void main(String[] args){ (new Test().new Son()).thinking();
} }
|
在Java程序中,可以通过“super”
关键字很方便地调用到父类中的方法,但却无法访问祖类的方法呢?
原因是在Son
类的thinking()
方法中根本无法获取到一个实际类型是GrandFather
的对象引用,而invokevirtual
指令的分派逻辑是固定的,只能按照方法接收者的实际类型进行分派,这个逻辑完全固化在虚拟机中,程序员无法改变。
基于栈的字节码解释执行引擎
前面一章我们提到,基于栈的解释执行,是真正地执行方法地字节码。这里的栈是Part 1中提到的,栈帧中的操作数栈。
解释执行
先通过 javac
将代码编译成字节码,虚拟机再通过加载字节码文件,解释执行字节码文件生成机器码,解释执行的流程如下:
1
| 词法分析 -> 语法分析 -> 形成抽象语法树 -> 遍历语法树生成线性字节码指令流
|
指令集分类
基于栈的指令集
- 优点:
- 可移植:寄存器由硬件直接提供,程序如果直接依赖这些硬件寄存器,会不可避免的受到硬件的约束;
- 代码更紧凑:字节码中每个字节对应一条指令,多地址指令集中还需要存放参数;
- 编译器实现更简单:不需要考虑空间分配问题,所需的空间都在栈上操作。
- 缺点: 执行速度稍慢
- 完成相同的功能,需要更多的指令,因为出入栈本身就产生相当多的指令;
- 频繁的栈访问导致频繁的内存访问,对于处理器而言,内存是执行速度的瓶颈。
示例:两数相加
1 2 3 4
| iconst_1 iconst_1 iadd istore_0
|
基于寄存器的指令集
示例:两数相加
执行过程分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class Architecture { public int calc() { int a = 100; int b = 200; int c = 300; return (a + b) * c; }
public static void main(String[] args) { Architecture architecture = new Architecture(); architecture.calc(); }
}
|
参考链接
- 《深入理解Java虚拟机》
- https://github.com/TangBean/understanding-the-jvm/blob/master/Ch2-Java虚拟机程序执行/02-虚拟机字节码执行引擎_01-方法调用.md
- https://github.com/TangBean/understanding-the-jvm/blob/master/Ch2-Java虚拟机程序执行/02-虚拟机字节码执行引擎_02-基于栈的字节码解释执行引擎.md