概述

动态链接(或指向运行时常量池的方法引用)

  1. 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic指令
  2. 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用 ```java public class DynamicLinkingTest {

    int num = 10;

    public void methodA(){

    1. System.out.println("methodA()....");

    }

    public void methodB(){

    1. System.out.println("methodB()....");
    2. methodA();
    3. num++;

    }

}

  1. 对应的字节码

Classfile /F:/IDEAWorkSpaceSourceCode/JVMDemo/out/production/chapter05/com/atguigu/java1/DynamicLinkingTest.class Last modified 2020-11-10; size 712 bytes MD5 checksum e56913c945f897c7ee6c0a608629bca8 Compiled from “DynamicLinkingTest.java” public class com.atguigu.java1.DynamicLinkingTest minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool:

1 = Methodref #9.#23 // java/lang/Object.”“:()V

2 = Fieldref #8.#24 // com/atguigu/java1/DynamicLinkingTest.num:I

3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;

4 = String #27 // methodA()….

5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V

6 = String #30 // methodB()….

7 = Methodref #8.#31 // com/atguigu/java1/DynamicLinkingTest.methodA:()V

8 = Class #32 // com/atguigu/java1/DynamicLinkingTest

9 = Class #33 // java/lang/Object

10 = Utf8 num

11 = Utf8 I

12 = Utf8

13 = Utf8 ()V

14 = Utf8 Code

15 = Utf8 LineNumberTable

16 = Utf8 LocalVariableTable

17 = Utf8 this

18 = Utf8 Lcom/atguigu/java1/DynamicLinkingTest;

19 = Utf8 methodA

20 = Utf8 methodB

21 = Utf8 SourceFile

22 = Utf8 DynamicLinkingTest.java

23 = NameAndType #12:#13 // ““:()V

24 = NameAndType #10:#11 // num:I

25 = Class #34 // java/lang/System

26 = NameAndType #35:#36 // out:Ljava/io/PrintStream;

27 = Utf8 methodA()….

28 = Class #37 // java/io/PrintStream

29 = NameAndType #38:#39 // println:(Ljava/lang/String;)V

30 = Utf8 methodB()….

31 = NameAndType #19:#13 // methodA:()V

32 = Utf8 com/atguigu/java1/DynamicLinkingTest

33 = Utf8 java/lang/Object

34 = Utf8 java/lang/System

35 = Utf8 out

36 = Utf8 Ljava/io/PrintStream;

37 = Utf8 java/io/PrintStream

38 = Utf8 println

39 = Utf8 (Ljava/lang/String;)V

{ int num; descriptor: I flags:

public com.atguigu.java1.DynamicLinkingTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object.”“:()V 4: aload_0 5: bipush 10 7: putfield #2 // Field num:I 10: return LineNumberTable: line 7: 0 line 9: 4 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/atguigu/java1/DynamicLinkingTest;

public void methodA(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String methodA()…. 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 12: 0 line 13: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this Lcom/atguigu/java1/DynamicLinkingTest;

public void methodB(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String methodB()…. 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: aload_0 9: invokevirtual #7 // Method methodA:()V 12: aload_0 13: dup 14: getfield #2 // Field num:I 17: iconst_1 18: iadd 19: putfield #2 // Field num:I 22: return LineNumberTable: line 16: 0 line 18: 8 line 20: 12 line 21: 22 LocalVariableTable: Start Length Slot Name Signature 0 23 0 this Lcom/atguigu/java1/DynamicLinkingTest; } SourceFile: “DynamicLinkingTest.java”

  1. 1. 在字节码指令中,`methodB()` 方法中通过 `invokevirtual #7` 指令调用了`methodA()` ,那么 #7 是个啥呢?
  2. 2. 往上面翻,找到常量池的定义:第16行,`#7 = Methodref #8.#31`
  3. - 先找 #8 :
  4. - `#8 = Class #32` :去找 #32
  5. - `#32 = Utf8 com/atguigu/java1/DynamicLinkingTest`
  6. - 结论:通过 #8 我们找到了 `DynamicLinkingTest` 这个类
  7. - 再来找 #31:
  8. - `#31 = NameAndType #19:#13` :去找 #19 和 #13
  9. - `#19 = Utf8 methodA `:方法名为 methodA
  10. - `#13 = Utf8 ()V` :方法没有形参,返回值为 void
  11. 3. 结论:通过 #7 我们就能找到需要调用的 `methodA()` 方法,并进行调用
  12. 4. 在上面,其实还有很多符号引用,比如 ObjectSystemPrintStream 等等
  13. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/21464164/1657785416705-72e36730-85e0-49fe-acb4-9b31c244798d.png#clientId=u9abdb389-245f-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=398&id=u98377e8e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1014&originWidth=2278&originalType=url&ratio=1&rotation=0&showTitle=false&size=530773&status=done&style=none&taskId=uff8cf6b7-0e8e-49e6-9c44-1134773cb43&title=&width=893.3333740234375)
  14. <a name="Zk263"></a>
  15. # 方法调用
  16. <a name="iYfD2"></a>
  17. ## 静态链接与动态链接
  18. JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
  19. - **静态链接**:
  20. 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期确定,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
  21. - **动态链接**:
  22. 如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
  23. <a name="MoXMO"></a>
  24. ## 早期绑定与晚期绑定
  25. > 静态链接与动态链接针对的是方法。早期绑定和晚期绑定范围更广。早期绑定涵盖了静态链接,晚期绑定涵盖了动态链接。
  26. 静态链接和动态链接对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。**绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程**,这仅仅发生一次。
  27. - **早期绑定**
  28. 早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就**可以使用静态链接的方式将符号引用转换为直接引用**。
  29. - **晚期绑定**
  30. 如果被调用的方法在编译期无法被确定下来,**只能够在程序运行期根据实际的类型绑定相关的方法**,这种绑定方式也就被称之为晚期绑定。
  31. 用多态来举例子
  32. ```java
  33. class Animal {
  34. public void eat() {
  35. System.out.println("动物进食");
  36. }
  37. }
  38. interface Huntable {
  39. void hunt();
  40. }
  41. class Dog extends Animal implements Huntable {
  42. @Override
  43. public void eat() {
  44. System.out.println("狗吃骨头");
  45. }
  46. @Override
  47. public void hunt() {
  48. System.out.println("捕食耗子,多管闲事");
  49. }
  50. }
  51. class Cat extends Animal implements Huntable {
  52. public Cat() {
  53. super();//表现为:早期绑定
  54. }
  55. public Cat(String name) {
  56. this();//表现为:早期绑定
  57. }
  58. @Override
  59. public void eat() {
  60. super.eat();//表现为:早期绑定,直接调用的父类的,Cat又没有子类,又只有一个父类,编译时就明确了
  61. System.out.println("猫吃鱼");
  62. }
  63. @Override
  64. public void hunt() {
  65. System.out.println("捕食耗子,天经地义");
  66. }
  67. }
  68. public class AnimalTest {
  69. public void showAnimal(Animal animal) {
  70. animal.eat();//表现为:晚期绑定,因为无法在编译期间知道这个Animal到底是Cat还是Dog还说说就是一个Animal
  71. }
  72. public void showHunt(Huntable h) {
  73. h.hunt();//表现为:晚期绑定,Huntable是一个接口,在编译时更是不知道穿的是个什么
  74. }
  75. }

查看字节码指令,可以看到方法调用指令。(这里就不贴上字节码指令了,占地方)

  • invokevirtual:体现为晚期绑定
  • invokeinterface:也体现为晚期绑定
  • invokespecial:体现为早期绑定

    多态与绑定

    多态与绑定

  1. 随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
  2. Java中任何一个普通的方法其实都具备虚函数的特征。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

    猫,狗都继承自动物,动物有一个方法eat,调用动物时,猫狗都是动物,eat也不确定是猫狗本身的eat,还是动物的eat,所以是虚方法,晚期绑定

虚方法与非虚方法

  1. 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
  2. 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
  3. 其他方法称为虚方法。

关于多态看这里

虚拟机中调用方法的指令

  • 普通指令:
  1. invokestatic:调用静态方法,解析阶段确定唯一方法版本
  2. invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
  3. invokevirtual:调用所有虚方法
  4. invokeinterface:调用接口方法

    如果父类有A方法,但是子类没有重写A,子类可以调用父类的A方法,在子类调用A方法的时候,编译器认为你可能调用子类的A方法(即使子类没有重写,也会认为),所以编译期间确定不下来,就是虚方法。 如果这个父类的A方法是一个被final修饰的方法,子类压根不能重写,子类调用A时,编译器也会认为你可能调用子类的A方法,所以指令就会是invokevirtual。但实际上final因为不能被重写,在编译时就知道具体是哪个,所以被final修饰的方法其实是非虚方法。 如果在子类调用super指向父类的A方法,调用指令就不是这个了,而是invokespecial

  • 动态调用指令

invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预。而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。

关于 invokedynamic 指令

  1. JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现【动态类型语言】支持而做的一种改进。
  2. 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
  3. Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。

image.png

动态语言和静态语言

  1. 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
  2. 说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
  • Java:String info = "mogu blog";(Java是静态类型语言的,会先编译就进行类型检查)
  • JS:name = "shkstart"; var name = 10;(运行时才进行检查)
  • Python:info = 130.5(运行时才检查)

    Java语言中方法重写的本质

  1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验。
    • 如果通过则返回这个方法的直接引用,查找过程结束
    • 如果不通过,则返回java.lang.IllegalAccessError 异常
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。

    比如,动物类有eat方法,猫类继承动物类,但是没有重写eat方法,猫调用eat方法,会先在猫类里面找eat,没有找到就去猫的父类动物类找eat。

  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

    上面这个过程称为动态分派

IllegalAccessError介绍

  1. 程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
  2. 比如,你把应该有的jar包放从工程中拿走了,或者Maven中存在jar包冲突

    虚方法表

  3. 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,非虚方法不会出现在表中。使用索引表来代替查找。【上面动态分派的过程,我们可以看到如果子类找不到,还要从下往上找其父类,非常耗时】

  4. 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
  5. 虚方法表是什么时候被创建的呢?虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的虚方法表也初始化完毕。

例子1

image.png
左边的表示Father的虚方法表,右边的是Son的虚方法表。
Son继承Father,Father有两个方法,Son也重写了这两个方法。
因为最终继承的都是Objcet,Father和Son蓝色的方法都是没有重写的,蓝色的是Objcet的方法,所以在虚方法表中,Father,Son的蓝色部分方法直接指向Objcet数据类型。
白色的部分因为两个类都有显式的写了这个两个方法,所以,实际指向Father和Son。

例子2

狗类 继承 宠物类 继承 动物类
动物类有A1,A2,A3,A4,A5方法
宠物类重写了A1,A2,自己有加了P1,P2,P3方法
狗类重写了A1,P1,自己加了D1,D2方法。
现在狗类的虚方法表指向如下

方法 指向
A1
A2 宠物
A3 动物
A4 动物
A5 动物
P1
P2 宠物
P3 宠物
D1
D2