在jvm中的方法分为虚方法和非虚方法
非虚方法
如果方法在编译期间就确定了具体的调用版本,这个版本在运行时是不可变得,这样的方法称为非虚方法,具体有静态方法(不能被重写),私有方法(不能被重写),final方法(不能被重写),实例构造器(不能被重写),父类方法(明确地使用super的方法调用父类方法)都是非虚方法。
虚方法
除了非虚方法外的都是虚方法(除了final修饰的之外)
虚与非虚方法对应的普通字节码指令
非虚方法对应的字节码指令:
invokestatic字节码指令:调用静态方法,解析阶段确定唯一方法版本
invokespecial字节码指令:调用init方法,私有及父类方法。
虚方法对应的字节码指令:
invokevirtual字节码指令:调用所有虚方法(除了final修饰的之外)
invokeinterface字节码指令:调用接口方法
下面以实际的代码来做实例
package com.lydon.test;
public class Son extends Father{
public Son(){
super();
}
public Son(String name){
this();
}
/**
* 子类自己的静态方法
*/
public static void showStatic(){
System.out.println("son static");
}
private void showPrivate(String str){
System.out.println("son private"+str);
}
public void info(){
System.out.println("son info()");
}
public void show(){
//静态方法
showStatic();
super.showStatic();
//私有方法
showPrivate("");
super.showCommon();
//final方法,因为final不能被重写,所以这里不加super也是非虚方法
super.showFinal();
showCommon();
info();
}
public static void main(String[] args) {
Son son = new Son();
son.show();
}
}
class Father{
public Father(){
System.out.println("father的构造器");
}
public static void showStatic(){
System.out.println("father static");
}
public final void showFinal(){
System.out.println("father show final");
}
public void showCommon(){
System.out.println("father 普通方法");
}
}
查看Son类的show方法的字节码:
一句话总结,不管是子类还是父类,凡是能被重写的方法就是虚方法,子类如果使用super显示调用父类的方法,则就不是虚方法。
动态字节码指令
除了上述四种普通字节码指令外,还有一种动态字节码指令invokedynamic,它在jdk1.8之前不能通过代码的方式生成,在jdk1.8之后因为lambda的引入后(匿名函数不能确定具体的类型),才可以生成,而lambda表达式的引入,使得java从一个纯粹的静态语言(类型的检查是在编译期就检查)变成带有一点动态语言(例如js,python)的语言。示例如下:
package com.lydon.test;
interface Func{
public boolean func(String str);
}
public class Lambda {
public void lambda(Func func){
return;
}
public static void main(String[] args) {
Lambda lambda = new Lambda();
//匿名函数
Func func=s->{
return true;};
lambda.lambda(func);
//匿名函数
lambda.lambda(s->{
return false;});
}
}
方法调用的本质
在jvm中,非虚方法能确定地找到对应的方法,但对于虚方法来说,由于不能确定调用的虚方法是调用的谁,所以在jvm解析到方法调用的字节码时,会经历如下过程:
- 找到操作数栈顶的第一个元素所执行对象的实际类型C
- 根据类型C去常量池中找描述和名称都符合的方法,如果找到则进行访问权限校验,如果通过则返回该方法的直接引用,如果不通过则抛出java.lang.IllegalAccessError异常
- 如果在步骤2没找到,则说明该方法不在C本类中,那就有可能在C的父类中,于是就需要对C的父类一次执行2的操作
- 如果上述都没有找到,证明调用的方法是接口(抽象方法),没有进行实现,则会抛出java.lang.AbstractMethodError
一般abstractMethodError错误发生在jar包版本不一致的情况,例如有jar包a的1.0版本,jarb包b1.0在基于a1.0写了实现它某个接口的方法method1,并编译通过。而后面a调整了该接口,升级到a2.0,但此时b还是没变,此时引用了a2.0和b1.0程序的c在调用b的method1方法时,就会出现这个错误。
在实际场景来说,对于虚方法如果每次都要经历上述寻找的过程,则会影响到执行效率,因此为了提高性能,jvm采用在类的方法区建立虚方法表(只有虚方法的映射),使用索引表来代替查找,提高效率。
虚方法表
- 每个类中都有虚方法表,表中存放着虚方法的实际入口,毕竟有些虚方法会是父类的方法。
- 虚方法表会在类加载时的链接阶段被创建并初始化,类的变量初始化完成后,jvm会把该类的方法表也初始化完毕。
虚方法表的示例图如下: