文章目录
方法调用并不等同于方法的执行。方法调用的唯一任务就是确定调用的是哪一个方法。一切方法调用在class文件里面存的都是符号引用,而不是方法在内存中的入口地址。
一、解析
解析阶段干什么:
解析阶段是解析在程序运行之前就确定的东西。
解析阶段解析的东西:
私有方法,静态方法这些非虚方法。
解析阶段使用到的命令:
- invokespecial:调用私有方法、父类方法、实例构造器<init>
- invokestatic:调用静态方法。但不包括final修饰的静态方法(用的是invokespecial)
调用字节码的指令
调用字节码的指令除了上面那两个,还有
- invokevirtual :调用所有的虚方法 与final修饰的非静态方法(final修饰的非静态方法不是虚方法)。
- invokeinterface: 调用接口方法。会在运行时确定一个实现此接口的对象
- invokedynamic:先在运行时动态解析限定符所引用的方法,然后再执行该方法。前面4条指令都是固化在虚拟机内部的,而这条指令的分派逻辑是由用户所设定的引导程序决定的(这里不太明白)。lambda表达式就用到了这条指令。
这里是使用这5个指令的例子:
public class Main {
public static void sayHello() {
System.out.println("Hello World!");
}
public static final void sayHello2() {
System.out.println("Hi,world!");
}
public final void sayHello3() {
System.out.println("HaHa!!");
}
public static void dynamicMethod(IA ia){
ia.interfaceA();
}
public static void main(String[] args) {
sayHello();//这里使用的是invokestatic命令
sayHello2();//这里使用的是invokestatic命令
Main m = new Main();//调用<init>()是使用invokespecial
m.sayHello3();//使用的是invokevirtual命令,但是sayHello3是个非虚方法,因为它用final修饰的
IA ia = new IA() {
@Override
public void interfaceA() {
System.out.println("我是接口方法");
}
};
ia.interfaceA();//这里使用的是invokeinterface命令
AbstractClass ac=new AbstractClass() {
@Override
public void abstractMethod() {
System.out.println("我是虚方法");
}
};
ac.abstractMethod();//使用的是invokevirtual指令
dynamicMethod(()->{ //使用lambda表达式时使用的是invokedynamic指令,调用dynamicMethod任然使用的是invokestatic指令
System.out.println("lambda表达式子");
});
IA temp=new IA(){//调用IA的实例构造器使用的是invokespecial指令
@Override
public void interfaceA() {
System.out.println("没有使用lambda表达式");
}
};
dynamicMethod(temp);//调用dynamicMethod任然使用的是invokestatic指令
}
}
interface IA {
void interfaceA();
}
abstract class AbstractClass {
public abstract void abstractMethod();
}
class SubClassA extends AbstractClass{
@Override
public void abstractMethod() {
System.out.println("SubClassA重写了父类方法");
}
}
class SubClassB extends AbstractClass {
@Override
public void abstractMethod() {
System.out.println("SubClassB重写了父类方法");
}
}
运行结果:
然后javap -verbose Main,得到结果如下(我只截了main方法的):
虚方法、非虚方法:
- 非虚方法:包括静态方法、私有方法。《java虚拟机规范》明确规定了final修饰的方法也为非虚方法。
- 虚方法:不是非虚方法的方法。
二、分派
按分派调用的方式可以分为静态和动态方式。按分派的宗量可分为单分派和多分派。
2.1 静态分派
首先来一个例子,看一下输出:
public class Main {
public void sayHello(AbstractClass ac) {
System.out.println("Hello World!");
}
public void sayHello(SubClassA a) {
System.out.println("SubClassA say hello");
}
public void sayHello(SubClassB b) {
System.out.println("SubClassB say hello");
}
public static void main(String[] args) {
Main m = new Main();
AbstractClass ac1 = new SubClassA();//静态类型是AbstractClass,实际类型是SubClassA
AbstractClass ac2 = new SubClassB();//静态类型是AbstractClass,实际类型是SubClassB
m.sayHello(ac1);//调用sayyHello方法时使用的是invokevirtual指令
m.sayHello(ac2);
}
}
abstract class AbstractClass {
public abstract void abstractMethod();
}
class SubClassA extends AbstractClass {
@Override
public void abstractMethod() {
System.out.println("SubClassA重写了父类方法");
}
}
class SubClassB extends AbstractClass {
@Override
public void abstractMethod() {
System.out.println("SubClassB重写了父类方法");
}
}
输出结果是:
静态类型和实际类型:
AbstractClass ac1 = new SubClassA();//静态类型是AbstractClass,实际类型是SubClassA
静态类型和实际类型的区别:
首先我们看静态类型变化和实际类型变化
AbstractClass ac=new SubClassA();
ac=new SubClassB();//实际类型变化
m.sayHello((SubClassA) ac);//静态类型变化,ac本身的静态类型是没有变化的
m.sayHello((SubClassB) ac);//静态类型变化,ac本身的静态类型是没有变化的
- 静态类型是在编译期就确定的,而实际类型是在运行期确定的。
重载是静态分派的典型应用。重载往往是找出最合适的匹配方法。重载方法的匹配顺序。
byte–>short–>int–>long–>float–>double–>对应的装箱类–>(现在可以上转型了)
例子:
import java.io.Serializable;
public class Main {
public void doSomething(char c) {
System.out.println("char c");
}
public void doSomething(int c) {
System.out.println("int c");
}
public void doSomething(long c) {
System.out.println("long c");
}
public void doSomething(float c) {
System.out.println("float c");
}
public void doSomething(double c) {
System.out.println("doulbe c");
}
public void doSomething(Double c) {
System.out.println("Double c");
}
public void doSomething(Object c) {
System.out.println("Object c");
}
public void doSomething(Character c) {
System.out.println("Character c");
}
public void doSomething(Serializable c) {
System.out.println("Serializable c");
}
public static void main(String[] args) {
Main m = new Main();
char c = 3;
m.doSomething(c);
}
}
2.2 动态分派
用一个例子来解释动态分派:
public class Main {
public static void main(String[] args) {
Main m = new Main();
AbstractClass a=new SubClassA();
AbstractClass b=new SubClassB();
a.abstractMethod();//解析后的指令都为Method AbstractClass.abstractMethod:()V
b.abstractMethod();//解析后的指令都为Method AbstractClass.abstractMethod:()V
}
}
class AbstractClass {
public void abstractMethod() {
}
}
class SubClassA extends AbstractClass {
@Override
public void abstractMethod() {
System.out.println("SubClassA重写了父类方法");
}
}
class SubClassB extends AbstractClass {
@Override
public void abstractMethod() {
System.out.println("SubClassB重写了父类方法");
}
}
javap -verbose Main后的结果(注意25行和29行):
运行结果:
问题来了,既然解析是的指令都一样,为什么运行后的结果不一样呢??在说明为什么之前,我们先知道个概念:调用方法的对象称为接受者。
invokevirtual运行时的解析过程
- 找到操作数栈顶的第一个元素的实际类型,记做C。
- 如果在类型C中找到与常量中的描述符和简单名词相符的方法,则进行访问权限校验,如果通过就返回这个方法的直接引用,查找结束;如果不通过,则返回java.lang.IllegalAccessError。
- 否则,按照继承关系从下往上依次进行第2个步奏。
- 如果始终没找到合适的 方法,则抛出java.lang.AbstractMethodError。
2.3 单分派与多分派
方法的接受者与方法的参数统称为方法的宗量。
public class Main {
public static void main(String[] args) {
Father father=new Father();
Father son=new Son();
father.hardChoice(new _360());//Method Father.hardChoice:(L_360;)V
son.hardChoice(new QQ());// Method Father.hardChoice:(LQQ;)V
}
}
class QQ {
}
class _360 {
}
class Father{
public void hardChoice(QQ arg){
System.out.println("father choose qq");
}
public void hardChoice(_360 arg){
System.out.println("father choose 360");
}
}
class Son extends Father{
public void hardChoice(QQ arg){
System.out.println("son choose qq");
}
public void hardChoice(_360 arg){
System.out.println("son choose 360");
}
}
javap -verbose Main之后(注意24和35):
静态解析时依据的是静态类型和参数,运行时是依据接受者的实际类型.
目前(直至java1.8)为止java语言是一门静态多分派、动态单分派的语言。
2.4 虚拟机动态分派的实现
每个虚拟机对怎样实现动态分派是有所区别的。动态分派是一个频繁的动作。基于性能的考虑,最常用的“稳定手段”是在类的方法区中建立虚方法表(与之对应在invokeinterface时也会用到接口方法表)。
比如上面代码的虚方法表结构如下:
虚方法表存的是各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么父类和子类虚方法表中该方法的入口地址一样。
还要注意的是具有相同签名的方法,在子类和父类的虚方法表中都应当具有一样的索引序号。
三、动态语言支持
3.1 动态语言类型
什么是动态语言类型
类型检查的主体在程序运行期间,而不是编译期间。满足这个特征的语言有JavaScript、PHP、Clojure、Groovy、Jython、Python、Ruby。与之相反的就是静态类型语言,如Java、C++。
ECMAScripte动态类型语言与java等静态类型语言的区别
一句话:“变量无类型而变量值才有类型”
动态类型语言和静态类型语言各自的优缺点
静态类型语言最显著的好处就是提供了严谨的类型检查,这样与类型相关的问题在编码期间就能及时发现,利于稳定性及代码达到更大规模。动态类型语言在运行期间确定类型,这给开发人员提供了更大的灵活性,在某些静态语言需要大量代码来实现的功能,由动态语言编写会更加清晰和简洁,意味着开发效率的提升。
3.2 MethodHandle
MethodHandle是一种动态确定方法的机制。和C++中的方法指针类似。
例子:
import static java.lang.invoke.MethodHandles.lookup;//这和其他的import有什么区别
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
public class Main {
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).invokeExact("fengli");
}
/**
* @param reveiver 方法的接受者
* @return
*/
private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
//方法的返回类型为void,方法的参数为一个String类型的参数
MethodType mt = MethodType.methodType(void.class, String.class);
//在reveiver类型中查找叫做println的mt类型的方法。
return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
}
}
上面的功能也可以用反射完成。那么MethodHandle和反射的区别:
- 最大的区别是MethodHandle的设计是为了服务于所有java虚拟机之上的语言,包括Java语言。而反射值服务于java语言
- 反射和方法句柄都是模拟方法调用,反射是在java代码层次的模拟,而MethodHandle是在字节码层次的模拟。MethodHandles.lookup中的三个方法findStatic()、findVirtual()、findSpecial分别对应于字节码层面的invokestatic、invokevirtual、invokespecial。
- Reflection中的java.lang.reflect.Method对象包含的信息远比MethodHandle机制中的java.lang.invoke.MethodHandle多。也就是反射是重量级的 ,而MethodHandle是轻量级的。
3.3 invokedynamic指令
invokedynamic和方法句柄一样都是为了解决——如何把查找目标方法的决定权交给用户问题。MethodHandle和invokedynamic指令的区别:MethodHandle是用上层java代码和API实现的,而invokedynamci是用字节码和Class中其他属性、常量来实现的。
invokedynamic指令的介绍
含有invokedynamic的地方称为动态调用点,这个指令的第一个参数是CONSTANT_InvokeDynamic_info常量(包含引导方法、方法类型、名称)。
例子:
import static java.lang.invoke.MethodHandles.lookup;//这和其他的import有什么区别
import java.lang.invoke.*;
public class Main {
public static void main(String[] args) throws Throwable {
INDY_BootstrapMethod().invokeExact("fengli");
}
public static void testMethod(String s){
System.out.println("hello String:"+s);
}
/**
* 引导方法
* @param lookup
* @param name
* @param mt
* @return 表示真正要执行的目标方法调用
* @throws Throwable
*/
public static CallSite BootstrapMethod(MethodHandles.Lookup lookup,String name, MethodType mt) throws Throwable{
return new ConstantCallSite(lookup.findStatic(Main.class,name,mt));
}
private static MethodType MT_BootstrapMethod(){
return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",null);
}
private static MethodHandle MH_BootstrapMethod() throws Throwable{
return lookup().findStatic(Main.class,"BootstrapMethod",MT_BootstrapMethod());
}
private static MethodHandle INDY_BootstrapMethod() throws Throwable{
CallSite cs=(CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(),"testMethod",MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V",null));
return cs.dynamicInvoker();
}
}
首先,我们要知道仅依靠java语言的编译器javac是没有办法生成带有invokedynamic指令的字节码的(lambda表达式例外)。我们需要使用[INDY]将字节码转换为我们最终所要的字节码。
3.4 方法分派的例子
invokedynamic与其他4条invoke指令最大的差别就是它的分派逻辑并不是虚拟机决定的,而是有程序员决定的。
例子,获取族类的方法:
import static java.lang.invoke.MethodHandles.lookup;//这和其他的import有什么区别
import java.lang.invoke.*;
import java.lang.reflect.Field;
public class Main {
class GrandFather {
public void thinking() {
System.out.println("i am grandfather");
}
}
class Father extends GrandFather {
public void thinking() {
System.out.println("i am father");
}
}
class Son extends Father {
public void thinking() {
try {
//jdk1.7
/* MethodType mt = MethodType.methodType(void.class);
MethodHandle mh = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
mh.invoke(this);*/
//jdk1.8
MethodType mt = MethodType.methodType(void.class);
Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
IMPL_LOOKUP.setAccessible(true);
MethodHandles.Lookup lkp = (MethodHandles.Lookup)IMPL_LOOKUP.get(null);
MethodHandle h1 = lkp.findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
h1.invoke(this);
} catch (Throwable e) {
System.out.println("error");
}
}
}
public static void main(String[] args) throws Throwable {
(new Main()).new Son().thinking();
}
}
四、基于栈的字节码解释执行引擎
什么叫做解释执行、编译执行?
解释执行边解释边执行;而编译执行就是一次性将程序源码编译称目标代码,然后执行。
解释执行、编译执行的过程(从抽象语法树开始,上面那条分支是解释执行的,下面那条是编译执行的):
指令集分为基于栈的指令集和基于寄存器的指令集。下面用一个例子说明他俩。例子,计算“1+1”。
基于栈的指令集是这样的:
iconst_1
iconst_1
iadd
istore_0
基于寄存器的指令集是这样的:
mov eax,1
add eax,1
基于栈的指令集和基于寄存器的指令集的优缺点:
指令集 | 优点 | 缺点 |
---|---|---|
基于栈的指令集 | 可移植,代码相对紧凑,编译器实现更加简单 | 执行速度相对慢一些 |
基于寄存器的指令集 | 执行速度快 | 可移植性差 |
基于栈的解释器执行过程:
public class Main {
public static void main(String[] args){
System.out.println(new Main().calc());
}
public int calc(){
int a=100;//java虚拟机会自动选择最合适的指令,比如这里是sipush
int b=200;
int c=300;
return (a+b)*c;
}
}
javap -v -p后:
字节码执行情况图:
需要注意的是实际虚拟机实现可能会和概念模型有很大差距。