动态链接–指向运行时常量池的方法引用
- 每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的为了支持当前方法的代码能够实现动态链接(Dynamic Linking),如invokednamic指令。
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中,比如一个方法调用了另一个方法,就是通过常量池中方法的符号引用来表示的,动态链接的作用是为了将这些符号引用转换为调用方法的直接引用。
为什么需要常量池?
常量池的作用,是为了提供一些符号和常量,便于指令的识别
方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关,绑定机制分为早期绑定和晚期绑定,绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
- 早期绑定:指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也可以使用静态链接的方式将符号引用转换为直接引用
- 晚期绑定:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,称为晚期绑定
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期间保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接
动态链接
如果被调用方法在编译期无法被确定下来,只能够在程序运行期调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接
class Animal {
public void eat(){
System.out.println("动物进食");
}
}
interface Huntable {
void hunt();
}
class Dog extends Animal implements Huntable {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
@Override
public void hunt() {
System.out.println("狗拿耗子,多管闲事");
}
}
class Cat extends Animal implements Huntable {
public Cat() {
super(); //早期绑定
}
public Cat(String name) {
this(); //早期绑定
}
@Override
public void eat() {
super.eat(); //早期绑定
System.out.println("猫吃鱼");
}
@Override
public void hunt() {
System.out.println("猫拿耗子,天经地义");
}
}
public class AnimalTest {
public void showAnimal(Animal animal) {
animal.eat(); //晚期绑定
}
public void showHunt(Huntable h) {
h.hunt(); //晚期绑定
}
}
虚方法和非虚方法
-
非虚方法:方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这种方法称为非虚方法。
-
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
-
其它方法为虚方法
-
普通调用指令
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用方法,私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
-
动态调用指令
- invokedynamic:动态解析出需要调用的方法,然后执行
普通调用指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本,其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法
静态类型语言是判断变量自身的类型信息,动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息。
- invokedynamic:动态解析出需要调用的方法,然后执行
class Father {
public Father() {
System.out.println("father构造器");
}
public static void showStatic(String str) {
System.out.println("father " + str);
}
public final void showFinal() {
System.out.println("father show final");
}
public void showCommon() {
System.out.println("father 普通方法");
}
}
public class Son extends Father{
public Son() {
super();
}
public Son(int age) {
this();
}
//非重写父类的方法,静态方法不允许重写
public static void showStatus(String str) {
System.out.println("son " + str);
}
private void showPrivate(String str) {
System.out.println("son private " + str);
}
public void show() {
//invokestatic
showStatic("lotus.com");
//invokestatic
super.showStatic("goods!");
//invokespecial
showPrivate("hellow!");
//invokespecial
super.showCommon();
//invokevirtual,此方法声明有final,不能被子类重写,此方法为非虚方法
showFinal();
//invokevirtual
showCommon();
info();
MethodInterface in = null;
//invokeinterface
in.methodA();
}
private void info() {
}
public void display(Father f) {
f.showCommon();
}
public static void main(String[] args) {
Son so = new Son();
so.show();
}
}
interface MethodInterface {
void methodA();
}
@FunctionalInterface
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();
//invokedynamic
Func func = s-> {
return true;
};
//invokedynamic
lambda.lambda(s-> {
return true;
});
}
}
方法重写本质
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C
- 如果过程结束,不通过类型C中找到与常量中的描述符符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找不到,则返回java.lang.IllegalAccessError异常
- 否则,按继承关系从下向上依次对C的各个父类进行第2步搜索和验证过程
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
虚方法表
- 在面向对象的编程中,会频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话,就可能影响到执行效率,因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(非虚方法不会出现在表中)来实现,使用索引表代替查找
- 每个类中都有一个虚方法表,表中存放各个方法的实际入口
- 虚方法表会在类加载的链接阶段被创建并开始初使化,类的变量初使值准备完成之后,JVM会把该类的方法一也初始化完毕
方法返回地址
- 存放调用该方法的PC寄存器的值
- 一个方法结束,有两种方式:
- 正常执行完成
- 一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定
- 在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short,int类型时),lreturn,freturn,dreturn以及areturn,另外还有一个return指令供声明为void方法,实例化初始化方法、类和接口的初始化方法使用。
- 出现未处理的异常,非正常退出
- 方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。
- 正常执行完成
- 无论哪种方式退出,在方法退出后都返回到该方法被调用的位置,方法正常退出时,调用者PC寄存器的值做为返回地址,好调用该方法的指令的下一条指令地址,而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
import java.io.FileReader;
import java.io.IOException;
import java.util.Date;
/**
* Administrator
* 2024/5/17
*/
public class ReturnAddressTest {
//ireturn
public boolean methodBoolean() {
return false;
}
//ireturn
public byte methodByte(){
return 0;
}
//ireturn
public short methodShort(){
return 0;
}
//ireturn
public char methodChar(){
return 'a';
}
//ireturn
public int methodInt(){
return 0;
}
//lreturn
public long methodLong(){
return 0L;
}
//freturn
public float methodFloat(){
return 0.0f;
}
//dreturn
public double methodDouble(){
return 0.0;
}
//areturn
public String methodString() {
return null;
}
//areturn
public Date methodDate(){
return null;
}
//return
public void methodVoid(){
}
static {
int i = 10;
}
public void method2() {
methodVoid();
try {
method1();
} catch (IOException e) {
e.printStackTrace();
}
}
public void method1() throws IOException {
FileReader fr = new FileReader("lotus.txt");
char[] cBuffers = new char[1024];
int len;
while ((len = fr.read(cBuffers))!=-1) {
String str = new String(cBuffers,0,len);
System.out.println(str);
}
fr.close();
}
public static void main(String[] args) {
}
}
方法返回地址本质上,方法的退出就是当前栈帧出栈的过程,此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去
正常完成出口和异常完成出口区别:通过异常完成出口退出的不会给他的上层调用者产生任何返回值