方法重载和方法重写 的原理

方法重载和方法重写 的原理

前言
JVM执行字节码指令是基于栈的架构,就是说所有的操作数都必须先入栈,然后再根据需要出栈进行操作计算,再把结果进行入栈,这个流程和基于寄存器的架构是有本质区别的,而基于寄存器架构来实现,在不同的机器上可能会无法做到完全兼容,这也是Java会选择基于栈的设计的原因之一。

思考
我们思考下,当我们调用一个方法时,参数是怎么传递的,返回值又是怎么保存的,一个方法调用之后又是如何继续下一个方法调用的呢?调用过程中肯定会存储一些方法的参数和返回值等信息,这些信息存储在哪里呢?

我们知道,每次调用一个方法就会产生一个栈帧,所以我们肯定可以想到栈帧就存储了所有调用过程中需要使用到的数据。现在就让我们深入的去了解一下Java虚拟机栈中的栈帧吧。

栈帧
当我们调用一个方法的时候,就会产生一个栈帧,当一个方法调用完成时,它所对应的栈帧将被销毁,无论这种完成是正常的还是突然的(抛出一个未捕获的异常)。

每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、方法返回地址(Return Address)和额外的附加信息。

在给定的线程当中,永远只有一个栈帧是活动的,所以活动的栈帧又称之为当前栈帧,而其对应的方法则称之为当前方法,定义了当前方法的类则称之为当前类。当一个方法调用结束时,其对应的栈帧也会被丢弃。

局部变量表(Local Variables)
局部变量表是以数组的形式存储的,而且当前栈帧的方法所需要分配的最大长度是在编译时就确定了。局部变量表通过index来寻址,变量从index[0]开始传递。

局部变量表的数组中,每一个位置可以保存一个32位的数据类型:boolean、byte、char、short、int、float、reference或returnAddress类型的值。而对于64位的数据类型long和double则需要两个位置来存储,但是因为局部变量表是属于线程私有的,所以虽然被分割为2个变量存储,依然不用担心会出现安全性问题。

对于64位的数据类型,假如其占用了数组中的index[n]和index[n+1]两个位置,那么不允许单独访问其中的某一个位置,Java虚拟机规范中规定,如果出现一个64位的数据被单独访问某一部分时,则在类加载机制中的校验阶段就应该抛出异常。

Java虚拟机在方法调用时使用局部变量进行传递参数。在类方法(static方法)调用中,所有参数都以从局部变量中的index[0]开始进行参数传递。而在实例方法调用上,index[0]固定用来传递方法所属于的对象实例,其余所有参数则在从局部变量表内index[1]的位置开始进行传递。

注意:局部变量表中的变量不可以直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数才能使用

操作数栈(Operand Stacks)
操作数栈,在上下文语义清晰时,也可以称之为操作栈(Operand Stack),是一个后进先出(Last In First Out,LIFO)栈,同局部变量表一样,操作数栈的最大深度也是在编译时就确定的。

操作数栈在刚被创建时(也就是方法刚被执行的时候)是空的,然后在执行方法的过程中,通过虚拟机指令将常量/值从局部变量表或字段加载到操作数栈中,然后对其进行操作,并将操作结果压入栈内。

操作数堆栈上的每个条目都可以保存任何Java虚拟机类型的值,包括long或double类型的值。

注意:我们必须以适合其类型的方式对操作数堆栈中的值进行操作。例如,不可能将两个int类型的值压入栈后将其视为long类型,也不可能将两个float类型值压入栈内后使用iadd指令将其添加。

往期面试题:001期~180期汇总

动态连接(Dynamic Linking)
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

在Class文件中的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种就称为静态解析。而另外一部分则会在每一次运行期间才会转化为直接引用,这部分就称为动态连接。

方法返回地址
当一个方法开始执行后,只有两种方式可以退出:一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。

正常退出(Normal Method Invocation Completion)
如果对当前方法的调用正常完成,则可能会向调用方法返回一个值。当被调用的方法执行其中一个返回指令时,返回指令的选择必须与被返回值的类型相匹配(如果有的话)。

方法正常退出时,当前栈帧通过将调用者的pc程序计数器适当的并跳过当前的调用指令来恢复调用程序的状态,包括它的局部变量表和操作数堆栈。然后继续在调用方法的栈帧来执行后续流程,如果有返回值的话则需要将返回值压入操作数栈。

异常终止(Abrupt Method Invocation Completion)
如果在方法中执行Java虚拟机指令导致Java虚拟机抛出异常,并且该异常没有在方法中处理,那么方法调用会突然结束,因为异常导致的方法突然结束永远不会有返回值返回给它的调用者。

往期面试题:001期~180期汇总

其他附加信息
这一部分具体要看虚拟机产商是如何实现的,虚拟机规范并没有对这部分进行描述。

方法调用流程演示
上面的概念听起来有点抽象,下面我们就通过一个简单的例子来演示一下方法的执行流程。

package com.zwx.jvm;

public class JVMDemo {
public static void main(String[] args) {
int sum = add(1, 2);
print(sum);
}

public static int add(int a, int b) {
    a = 3;
    int result = a + b;
    return result;
}

public static void print(int num) {
    System.out.println(num);
}

}
要想了解Java虚拟机的执行流程,那么我们必须要对类进行编译,得到字节码文件,执行如下命令:

javap -c xxxxxxJVMDemo.class >1.txt
将JVMDemo.class生成的字节码指令输出到1.txt文件中,然后打开,看到如下字节码指令:

Compiled from “JVMDemo.java”
public class com.zwx.jvm.JVMDemo {
public com.zwx.jvm.JVMDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.""😦)V
4: return

public static void main(java.lang.String[]);
Code:
0: iconst_1
1: iconst_2
2: invokestatic #2 // Method add:(II)I
5: istore_1
6: iload_1
7: invokestatic #3 // Method print:(I)V
10: return

public static int add(int, int);
Code:
0: iconst_3
1: istore_0
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: iload_2
7: ireturn

public static void print(int);
Code:
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iload_0
4: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
7: return
}
如果是第一次接触可能指令看不太懂,但是大致的类结构还是很清晰的,我们先来对用到的字节码指令大致说明一下:

iconst_i
表示将整型数字i压入操作数栈,注意,这里i的返回只有-1~5,如果不在这个范围会采用其他指令,如当int取值范围是[-128,127]时,会采用bipush指令。

invokestatic
表示调用一个静态方法

istore_n
这里表示将一个整型数字存入局部变量表的索引n位置,因为局部变量表是通过一个数组形式来存储变量的

iload_n
表示将局部变量位置n的变量压入操作数栈

ireturn
将当前方法的结果返回到上一个栈帧

invokevirtual
调用虚方法

了解了字节码指令的大概意思,接下来就让我们来演示一下主要的几个执行流程:

1、代码编译之后大致得到如下的一个Java虚拟机栈,注意这时候操作数栈都是空的(pc寄存器的值在这里暂不考虑 ,实际上调用指令的过程,pc寄存器是会一直发生变化的)
在这里插入图片描述

2、执行iconst_1和iconst_2两个指令,也就是从本地变量中把整型1和2两个数字压入操作数栈内:
在这里插入图片描述

3、执行invokestatic指令,调用add方法,会再次创建一个新的栈帧入栈,并且会将参数a和b存入add栈帧中的本地变量表
在这里插入图片描述

4、add栈帧中调用iconst_3指令,从本地变量中将整型3压入操作数栈
在这里插入图片描述

5、add栈帧中调用istore_0,表示将当前的栈顶元素存入局部变量表index[0]的位置,也就是赋值给a。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值