😉😉 学习交流群:
✅✅1:这是孙哥suns给大家的福利!
✨✨2:我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料
🥭🥭3:QQ群:583783824 📚📚 工作微信:BigTreeJava 拉你进微信群,免费领取!
🍎🍎4:本文章内容出自上述:Spring应用课程!💞💞
💞💞5:以上内容,进群免费领取呦~ 💞💞💞💞
文章目录
我们每天都在写方法的调用,但是我们能搞明白其中的原理和JVM当中的操作步骤么?这就是本文的意义。
官方说法:
在JVM中,将符号引用转换为调用方法的直接引用这个操作是跟JVM当中方法的绑定机制息息相关的。
说人话:
上边这段话是什么意思?我这里给大家解释一下,我们javap整理完毕字节码文件之后,我们会可以在任意一个方法中查看code下的字节码指令,很多字节码指令的后边都会跟#数字这么一个概念,这个就是符号引用,这个引用指向常量池。
所谓将符号引用转换为方法的直接引用,就是将这个字节码指令后边的符号引用,转变为真实的方法。
下列中的#3就是符号引用。
- public void methodB();
- descriptor: ()V
- flags: (0x0001) 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
从上述找一个例子的话,就是将偏移地址为9的字节码指令后边的#7这个符号引用用真实的方法字面量代替
官方说法:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
说人话:
静态链接:这种方式在编译阶段就已经把符号引用直接转换为了直接引用。
官方说法:
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
说人话:
动态链接:这种方式在运行阶段才能把符号引用直接转换为直接引用。
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。这个不论是编译器确定还是运行期确定都只会发生一次,不会修改。
对应的方法的绑定机制为:早期绑定 (Early Bindng)和晚期绑定(Late Binding)。
官方说法:
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
说人话:
早期绑定是和我们的静态绑定相对应的。
官方说法:
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定
说人话:
晚期绑定是和我们的动态绑定相对应的。
- 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{
- @Override
- public void 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();//晚期绑定
- }
-
- }
- class Animal {
- public void eat(){
- System.out.println("动物进食");
- }
- }
-
- interface Huntable{
- void hunt();
- }
-
- class Dog extends Animal implements Huntable{
- @Override
- public void eat(){
- super.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(){
- 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();//晚期绑定
- }
-
- }
光标放到cat这个类上查看他的jclasslib
invokeSpecial是早期绑定字节码指令,invokevirtual是晚期绑定的字节码指令。
随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性
既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
Java中任何一个普通的方法其实都具备虚函数的特征,也就是运行期才能确定下来,它们相当于c++语言中的虚函数 (c++中则需要使用关键字virtual来显式定义)。
如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。也就是一个方法不想被晚期绑定,直接把他给final修饰即可。
文章目录
JVM中的程序计数寄存器 (Program Counter Register)中(程序计数寄存器), Register 的命名源于CPU的寄存器,寄存器存储指令相关的现场信息, CPU只有把数据装载到寄存器才能够运行
这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子)并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。 是一个软件层面的概念
钩子怎么理解呢?烤鸭的时候一个钩子钩一只鸭子。在我们的程序中一行一行代码也是由PC寄存器这个钩子钩着,需要的时候提溜出来直接执行就好啦。
PC寄存器用来存储指向下一条指令的地址也即将要执行的指令代码。由执行引擎读取下一条指令
PC寄存器是每一个新城有一份。 在方法入栈之后,对应的是一个又一个的栈帧入栈。栈帧分为局部变量表、操作数栈、动态链接、方法返回值(一个栈帧对应一个方法)。栈帧中每一个指令呢,都会有一个行号的标识,PC寄存器存储了下一条指令的地址。执行引擎根据PC寄存器中的指令获取下一条指令,执行完毕之后再去PC寄存器中去取指令执行。
它是一块很小的内存空间储区域。小到几乎可以忽略不计(因为只存储下一条指令的地址)。也是运行速度最快的存储区域。
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的。生命周期与线程的生命周期保持一致(线程私有的都会和线程声明周期一致)
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法(上图中的红框区域)。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址。或者,如果是在执行native方法,则是未指定值 (undefined)
怎样理解栈的这个先进后出,后进先出呢?想想手枪中的子弹就明白了!
它是程序控制流的指示器,分支、循环.跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
它是唯一一个在Java 虚拟机规范中没有规定任何outotMemoryError情况的区域。
- public class Dashu {
- public static void main(String[] args) throws IOException {
- int i = 10;
- int j = 20;
- int k = i +j;
- }
- }
- 2023/10/24 22:45
. - 2023/10/09 11:20
.. - 2023/10/09 11:24 143,226 aaa.txt
- 2023/10/24 22:45 474 Dashu.class
- 2 个文件 143,700 字节
- 2 个目录 116,607,049,728 可用字节
-
- D:\code\study\hadoop\shit\target\classes>javap -verbose Dashu.class
- Classfile /D:/code/study/hadoop/shit/target/classes/Dashu.class
- Last modified 2023-10-24; size 474 bytes
- MD5 checksum 87503ee08b4b8f0b59290d1832fe49c7
- Compiled from "Dashu.java"
- public class Dashu
- minor version: 0
- major version: 52
- flags: ACC_PUBLIC, ACC_SUPER
- Constant pool:
- #1 = Methodref #3.#23 // java/lang/Object."
":()V - #2 = Class #24 // Dashu
- #3 = Class #25 // java/lang/Object
- #4 = Utf8
- #5 = Utf8 ()V
- #6 = Utf8 Code
- #7 = Utf8 LineNumberTable
- #8 = Utf8 LocalVariableTable
- #9 = Utf8 this
- #10 = Utf8 LDashu;
- #11 = Utf8 main
- #12 = Utf8 ([Ljava/lang/String;)V
- #13 = Utf8 args
- #14 = Utf8 [Ljava/lang/String;
- #15 = Utf8 i
- #16 = Utf8 I
- #17 = Utf8 j
- #18 = Utf8 k
- #19 = Utf8 Exceptions
- #20 = Class #26 // java/io/IOException
- #21 = Utf8 SourceFile
- #22 = Utf8 Dashu.java
- #23 = NameAndType #4:#5 // "
":()V - #24 = Utf8 Dashu
- #25 = Utf8 java/lang/Object
- #26 = Utf8 java/io/IOException
- {
- public Dashu();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."
":()V - 4: return
- LineNumberTable:
- line 3: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 5 0 this LDashu;
-
- public static void main(java.lang.String[]) throws java.io.IOException;
- descriptor: ([Ljava/lang/String;)V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=2, locals=4, args_size=1
- 0: bipush 10
- 2: istore_1
- 3: bipush 20
- 5: istore_2
- 6: iload_1
- 7: iload_2
- 8: iadd
- 9: istore_3
- 10: return
- LineNumberTable:
- line 5: 0
- line 6: 3
- line 7: 6
- line 9: 10
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 11 0 args [Ljava/lang/String;
- 3 8 1 i I
- 6 5 2 j I
- 10 1 3 k I
- Exceptions:
- throws java.io.IOException
- }
- SourceFile: "Dashu.java"
-
- D:\code\study\hadoop\shit\target\classes>
- public static void main(java.lang.String[]) throws java.io.IOException;
- descriptor: ([Ljava/lang/String;)V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=2, locals=4, args_size=1
- 0: bipush 10 //取出来一个数10
- 2: istore_1 //保存到索引为1的位置
- 3: bipush 20 //取出来一个数20
- 5: istore_2 //保存到索引为2的位置
- 6: iload_1 //从索引1处取值
- 7: iload_2 //从索引2处取值
- 8: iadd //进行相加的操作
- 9: istore_3 //存到索引为3的位置
- 10: return //表示main方法结束
- LineNumberTable:
- line 5: 0
- line 6: 3
- line 7: 6
- line 9: 10
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 11 0 args [Ljava/lang/String;
- 3 8 1 i I
- 6 5 2 j I
- 10 1 3 k I
- Exceptions:
- throws java.io.IOException
- }
上边我们注释的地方有一行一行的数字,这个数字就是指令地址或者叫偏移地址。 偏移地址或者指令地址就是PC寄存器中存储的结构。
指令地址的右侧就是执行引擎需要执行的具体的指令。具体的指令我们后边都会分析到。
PC寄存器当中存储的就是这些数字。执行引擎会去我们指令地址对应的指令位置取出来具体的指令内容(去我们的局部变量表中取指令)。然后去操作咱们的栈结构中的局部变量表、操作数栈实现数据的存取,然后将我们的字节码指令翻译成为机器指令,就可以让我们的CPU帮我们做运算了。
问题1:使用PC寄存器存储字节码指令地址有什么用呢?
问题2:为什么使用PC寄存器记录当前线程的执行地址呢?
这两个本质上是一个问题:
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
问题2:PC寄存器为什么会被设定为线程私有?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
CPU 时间片即 CPU 分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
但在微观上:由于只有一个 CPU,一次只能处理程序要求的一部分如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
并行就是多核CPU,每一个CPU都在执行具体的线程,这个叫做并行。
串行就是单核情况下,线程排好队,一次只有一个线程再跑
并发就是一个核CPU,多个线程来回切换抢占CPU资源。