第19章 方法调用和返回
当java虚拟机调用一个类方法时,它会基于对象引用的类型(编译时可知)来选择所调用的方法;相反,当虚拟机调用一个实例方法时,它会基于对象实际的类(只能在运行时得知)来选择所调用的方法。
最初,所有的调用指令都指向一个包含符号引用的常量池入口。当java虚拟机遇到一条调用指令时,如果还没有解析符号引用,那么虚拟机把符号引用作为执行指令调用执行过程的一部分。要解析一个符号引用,java虚拟机要确定被符号化引用的方法,然后再用一个直接引用来代替符号引用。直接引用就如同偏移量指针一样,如果将来再次使用该引用,它可以使虚拟机更快地调用这个方法。
在解析过程中,java虚拟机还将执行几次确认检验,以确保遵循java语言的规法和调用invoke指令的安全。一旦解析了一个方法后,java虚拟机就准备调用它;如果这个方法是一个实例方法,它必须在一个对象中被调用;对实例方法的调用,虚拟机需要在栈里存在一个对象引用(objectref);如果方法需要参数,那么虚拟机还需要在栈中存在该方法所需要的参数(args)。如果这个方法是一个类方法,虚拟机只需要栈中存在args参数。objectref和args必须在调用指令执行前,被其他指令压入所调用方法的操作数栈。
虚拟机为每一个调用的java方法(非本地方法)建立一个新的栈帧。栈帧包括:为方法的局部变量所预留的空间、该方法的操作数栈以及特定虚拟机实现需要的其他所有信息。局部变量表和操作数栈的大小在编译时计算出来,并放置到class文件中去。虚拟机借此可以了解方法的栈帧需要多少内存;当它调用一个方法时,它为该方法创建恰当大小的栈帧,再将新的栈帧压入java栈。
处理实例方法时,虚拟机把所调用方法栈帧中的操作数栈中弹出objectref和args。虚拟机把objectref作为局部变量0放到新的栈帧中,把所有的args作为局部变量1、2 、。。。objectref的值是隐式传递给所有实例方法的this指针。对于类方法,虚拟机只弹出参数,并将它们放到局部变量的0、1、2、。。。然后虚拟机把新的栈帧作为当前栈帧,并将PC寄存器(程序计数器)指向方法的第一条指令。
尽管通常使用invokevirtual指令调用实例方法,但在某些特定的情况中,也会使用另外两种操作码 -- invokespecial和invokeinterface。
java虚拟机总是直接调用类初始化方法(<clinit>()),类的初始化方法永远不会被任何字节码调用。在java虚拟机的指令集中,没人任何调用<clinit>()方法的指令。如果class文件尝试使用任何指令调用<clinit>()方法,会导致虚拟机抛出异常。
invokespecial和invokevirtual的主要区别在于:invokespecail通常根据引用的类型来选择方法,而不是根据对象的类型来选择。它使用静态(编译时)绑定而不是动态(运行时)绑定。
当根据引用的类型来调用实例方法,而不是根据对象的类来调用的时候,通常使用invokespecial指令,分为3种情况:
(1) 实例初始化方法(<init>())。
(2) 私有方法。
(3) 使用super关键字所调用的方法。
invokespecial调用<init>()方法:<init>()方法是编译器为构造方法和实例变量初始化放置代码的地方。class文件中,类会为每个构造方法提供一个 <init>()方法。就像每个类都至少会有一个构造方法一样,每个类都至少会有一个 <init>()方法,这些方法通常使用invokespecial指令调用。
只有创建一个新的实例的时候,才调用 <init>()方法。新创建对象的继承路径中,每个超类都至少会调用一个 <init>()方法。使用invokespecial指令调用 <init>()方法的原因在于,子类的 <init>()方法需要拥有调用超类的 <init>()方法的能力。当一个对象实例化时,虚拟机调用类中声明的 <init>()方法,这个 <init>()方法首先调用同一个类中的其他 <init>()方法或者超类的 <init>()方法,这个过程贯穿于对象的整个生命周期。
invokespecial调用私有方法:当使用 invokespecial调用私有方法时,虚拟机会按照引用的类型来选择调用的方法。
invokespecial和super关键字:指令invokevirtual只能调用当前类的方法,无法使用超类的方法。java虚拟机是否使用静态绑定来执行invokespecial指令(或者使用特殊的动态绑定)取决于所执行的类是否设定了ACC_SUPER标志。在jdk1.0.2版本以前,invokespecial指令的名称为invokenonvirtual,而且总会导致静态绑定的使用,结果是无法保证所有情况下的java语言语义的正确实现(指令集中的一个bug)。在jdk1.0.2版本中,invokenonvirtual指令更名为invokespecial,它的语义也改变了。此外,java class文件中的access_flags项中还加入了一个新的标志:ACC_SUPER。class文件的ACC_SUPER标志指明,java虚拟机使用哪一种语义来执行class文件中遇到的invokespecial指令。如果没有设置ACC_SUPER标志,虚拟机将会使用旧的语义(invokenonvirtual语义);如果设置了ACC_SUPER标志,虚拟机将使用新的语义。
invokespecial新的语义除了调用超类方法之外,其他情况一律使用静态绑定。当java虚拟机解析一个invokespecial指令中指向超类方法的符号引用时,它会动态搜寻当前类的超类,找到离得最近的超类中的该方法的实现。大多数情况下,虚拟机很可能发现最近的方法实现存在于符号引用中列出的超类中。另外一种情况是:子类编译后,继承结构中的某个超类实现发生了变化,而子类没有重新进行编译;此时子类的class文件中的符号引用会指向超类修改前的实现类,invokespecial新语义在执行时会动态进行搜索,以确保语义的正确性。
invokeinterface和invokevirtual的功能相同, 这两条指令的区别在于:当引用的类型为类的时候,使用invokevirtual;当引用的类型为接口时,使用invokeinterface指令。java虚拟机使用不同于类引用的操作码来调用接口引用的方法,这是因为java不能像使用类引用那样,使用许多与方法表偏移量相关的假设。对于类引用来说,无论对象实际的类是什么,方法在方法表中始终占据相同的位置。但对于接口引用来说,由于实现同一接口的类,可能扩展(extends)了不同的超类或者实现了其他的接口,位于不同类中的同一方法所占据的位置是不同的。
调用接口引用方法可能要比调用类引用方法慢。因为,当java虚拟机遇到invokevirtual指令时,它把实例方法的符号引用解析为直接引用,所生成的直接引用很可能是方法表中的一个偏移量,而且从此往后都可以使用同样的偏移量。但对于invokeinterface指令,虚拟机每一次遇到invokeinterface指令,都不得不重新搜索一遍方法表,因为虚拟机不能够假设这一次的偏移量与上一次的偏移量相同。
最快的指令是invokespecial和invokestatic,当java虚拟机为这些指令解析符号引用时,将符合引用转换为直接引用,所生成的直接引用将包含一个指向实际操作码的指针。
从方法中返回:每一种操作码对应一种返回的数据类型,它们都没有操作数,如果有返回值,必须被放置在操作数栈中。返回值从操作数栈中弹出,然后被压入调用方法(调用代码所在的方法)的栈帧的操作数栈中。弹出当前栈帧,调用方法的栈帧成为当前栈帧;程序计数器被重置,指向紧随调用返回方法那条指令的下一条指令。指令ireturn用于返回int、char、byte和short类型数据。
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作。在Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法的调用过程变得相对复杂,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。
所有方法调用中的目标方法在class文件中都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一可确定的调用版本,并且这个方法的调用版本是运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
在Java语言中,符合“编译期可知,运行期不可变”这个要求的方法有静态方法和私有方法两大类,前者与类型直接相关联,后者在外部不可被访问,这两种方法都不可能通过继承或者别的方式重写出其它版本,因此它们都适合在类加载阶段进行静态解析。
与之相对应,在Java虚拟机里提供了四条方法调用字节码指令,分别是:
a. invokestatic:调用静态方法
b. invokespecial:调用实例构造器<init>方法,私有方法和超类方法。
c. invokevirtual:调用虚方法。
d. invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
只要能被invokestatic与invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器和超类方法四类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以统称为非虚方法,与之相反,其它方法就称为虚方法(除去final方法)。
Java中的非虚方法除了使用invokestatic与invokespecial指令调用的方法之后还有一种,就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其它版本,所以也无须对方法接收进行多态选择,又或者说多态选择的结果是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法。
invokespecial的例外情况:调用超类方法时,class中保存的时编译时的超类方法类型,运行时,如果超类中的继承结构变化,将会动态使用相应的方法版本。
相关推荐
- **字段和方法信息**:描述了类中所有字段和方法的详细信息。 - **属性信息**:附加了其他相关信息,如注释等。 综上所述,《深入Java虚拟机》这本书覆盖了Java体系结构、平台无关性、安全性、网络移动性以及JVM...
深入理解 Java 虚拟机笔记 Java 虚拟机(JVM)是 Java 语言的运行环境,它负责解释和执行 Java 字节码。下面是 Java 虚拟机相关的知识点: 虚拟机内存结构 Java 虚拟机的内存结构主要包括以下几个部分: * 方法...
虚拟机栈的生命周期与线程相同,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 堆区是 JVM 的最大的一块内存区域,是被线程共享的区域,在虚拟机启动时创建。所有类的实例...
Java方法和本地方法是两种不同的调用机制。Java方法是用Java语言编写,被编译成字节码,独立于平台。而本地方法通常用C、C++或汇编语言编写,与特定的硬件平台紧密关联,存储在动态链接库中。使用本地方法虽然可能...
- **功能**: 每个线程都有自己的栈,用于存储局部变量和方法调用状态。 - **生命周期**: 与线程的生命周期一致。 - **异常**: 如果线程请求的栈深度大于虚拟机允许的最大深度,则会抛出`StackOverflowError`异常。 ...
### 学习笔记之Java虚拟机详解 #### 运行时数据区域概览 Java虚拟机(JVM)运行时数据区域主要包括以下几部分:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区以及运行时常量池。 1. **程序计数器**: -...
2. **本地方法栈**:与Java虚拟机栈类似,但它用来支持本地方法调用。 #### 五、基本数据类型与引用类型的内存布局 1. **基本数据类型**: - **整型**:`byte`、`short`、`int`、`long`分别占用1、2、4、8个字节...
理解这些概念对于深入学习Java和优化Java应用性能至关重要。在面试或日常工作中,掌握JVM的工作原理、JVM与操作系统、JRE和JDK的关系,可以帮助我们更好地理解Java程序的运行机制,解决性能问题,以及进行高效的代码...
《JVM:深入理解Java虚拟机》是一本深入解析Java虚拟机工作原理和技术细节的经典书籍。这份学习笔记将涵盖JVM的关键概念、架构以及它如何影响Java程序的性能。我们将探讨以下几个方面: 1. **JVM概述** Java虚拟机...
### 深入Java虚拟机JVM类加载学习笔记 #### 一、Classloader机制解析 在Java虚拟机(JVM)中,类加载器(ClassLoader)是负责将类的`.class`文件加载到内存中的重要组件。理解类加载器的工作原理对于深入掌握JVM以及...
在深入探讨JVM的高级特性和最佳实践之前,我们先来看看与Java虚拟机相关的几个重要知识点。 首先,JVM的内存模型是非常重要的一个概念。JVM内存模型定义了程序中各种变量的访问规则,它包含了堆(Heap)、栈(Stack...
它们协同工作,将.class文件加载到内存中形成运行时数据区的类变量和方法区。此外,用户还可以自定义类加载器。 2. **内存管理**:JVM内存主要分为堆内存和栈内存两大部分。堆内存用于存储对象实例,而栈内存则保存...
this关键字代表当前对象的引用,可以用来区分成员变量和方法形参之间的冲突,调用成员变量、方法和构造器。 【包(package)】 包是组织Java类和接口的一种方式,通过package关键字声明,如`package ...
### 环境变量配置及JVM虚拟机知识点解析 #### 一、环境变量配置 在计算机系统中,环境变量是操作系统提供的一种机制,用于存储关键的信息,这些信息可以被运行中的程序所访问。对于Java开发环境而言,正确的环境...
2. **虚拟机栈**:每个线程都有自己的虚拟机栈,当执行方法时,会创建一个栈帧,包含局部变量表、操作数栈、动态链接和方法出口等信息。栈帧随着方法调用的开始和结束而入栈和出栈。 3. **本地方法栈**:与虚拟机栈...
JVM内存区域主要分为五部分:程序计数器、虚拟机栈、本地方法栈、方法区和堆。 1. 程序计数器(PC Register):指令相关堆、方法:数据相关程序计数器较小的内存空间,当前线程执行的字节码的行号指示器;各线程...
### 安卓逆向学习笔记之FART主动调用组件设计和源码分析 #### 一、概述 本文档旨在探讨安卓逆向工程中的一个重要工具——FART(Find And Replace Tool),并着重介绍如何利用该工具进行主动调用组件的设计与源码...
- **局部变量表**: 存储方法参数和方法内部定义的局部变量。每个变量占用一个Slot,64位类型的变量如`long`和`double`会占用两个Slot。 - **操作数栈**: 用于临时存储计算过程中产生的中间结果,并参与方法调用时的...