JVM中方法调用的底层实现,看这一篇就够了
yuyutoo 2024-11-12 14:58 2 浏览 0 评论
方法调用的底层实现
我们编写的Java代码,经过编译后变成class文件,最后经过类加载器加载后进入了JVM的运行时数据区。
但作为程序员真正关心是代码的执行,代码的执行其实本质上是方法的执行,站在JVM 的角度归根到底还是字节码的执行。
main()方法是JVM指令执行的起点,JVM会创建一个main线程来执行main()方法,以触发JVM 一系列指令的执行,真正地把JVM跑起来。接着,在我们的代码中,就是方法调用方法的过程,所以了解方法在JVM 中的调用是非常必要的。
关于方法的调用,JVM中一共提供了5个字节码指令,来调用不同类型的方法:
- invokestatic:用于调用类的静态方法。
- invokespecial:用于调用对象的私有实例方法、构造器this()及父类构造器super()等。
- invokevirtual:用于调用对象的非私有实例方法,比如被修饰符public和protected修饰的方法,大多数方法调用属于这一种、
- invokeinterface:和invokevirtual指令类似,只不过这个指令用于调用接口的方法。
- invokedynamic:用于调用动态方法,常用于lambda表达式。
非虚方法
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,在类加载的解析阶段,就会把这些方法的符号引用解析为直接引用(即入口地址),这样的方法称为非虚方法。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类构造器4种,再加上被final修饰的方法(尽管它使用 invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用,不需要在方法运行时再去进行动态链接。
invokestatic
invokestatic用来调用类的静态方法。
package com.morris.jvm.methodinvoke;
public class InvokeStaticDemo {
public static void hello() {
}
public static void main(String[] args) {
hello();
}
}
main()方法中会调用静态方法hello(),对应的字节码如下:
0 invokestatic #2 <com/morris/jvm/methodinvoke/InvokeStaticDemo.hello>
3 return
这个方法调用在编译期间就明确以常量池项的形式固化在字节码指令的参数之中了。
invokespecial
invokespecial用于调用对象的私有实例方法、构造器this()及父类构造器super()等。
package com.morris.jvm.methodinvoke;
public class InvokeSpecialDemo {
private void hello() {
}
public static void main(String[] args) {
InvokeSpecialDemo invokeSpecialDemo = new InvokeSpecialDemo();
invokeSpecialDemo.hello();
}
}
main()方法创建了一个InvokeSpecialDemo对象,会调用InvokeSpecialDemo的默认构造方法(使用了invokespecial指令),调用私有方法hello()也使用了使用了invokespecial指令:
0 new #2 <com/morris/jvm/methodinvoke/InvokeSpecialDemo>
3 dup
4 invokespecial #3 <com/morris/jvm/methodinvoke/InvokeSpecialDemo.<init>>
7 astore_1
8 aload_1
9 invokespecial #4 <com/morris/jvm/methodinvoke/InvokeSpecialDemo.hello>
12 return
另外在InvokeSpecialDemo的默认构造方法中会调用父类Objcet的构造方法,也使用了invokespecial指令:
0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 return
虚方法
虚方法与非虚方法相反,在类加载时期,无法确定方法最终的调用版本,如果一个方法被重载了,需要根据传入参数的类型才能确定具体调用哪个方法。
invokevirtual
invokevirtual用于调用对象的非私有实例方法,比如被修饰符public和protected修饰的方法,大多数方法调用属于这一种(排除掉被final修饰的方法)。
package com.morris.jvm.methodinvoke;
public class InvokeVirtualDemo {
public static void main(String[] args) {
InvokeVirtualDemo invokeVirtualDemo = new InvokeVirtualDemo();
invokeVirtualDemo.hello();
}
public void hello() {
}
}
main()方法对应的字节码如下:
0 new #2 <com/morris/jvm/methodinvoke/InvokeVirtualDemo>
3 dup
4 invokespecial #3 <com/morris/jvm/methodinvoke/InvokeVirtualDemo.<init>>
7 astore_1
8 aload_1
9 invokevirtual #4 <com/morris/jvm/methodinvoke/InvokeVirtualDemo.hello>
12 return
invokeinterface
invokeinterface和invokevirtual指令类似,只不过这个指令用于调用接口的方法。
package com.morris.jvm.methodinvoke;
public class InvokeInterfaceDemo {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
}
};
r.run();
}
}
main()方法对应的字节码如下:
0 new #2 <com/morris/jvm/methodinvoke/InvokeInterfaceDemo$1>
3 dup
4 invokespecial #3 <com/morris/jvm/methodinvoke/InvokeInterfaceDemo$1.<init>>
7 astore_1
8 aload_1
9 invokeinterface #4 <java/lang/Runnable.run> count 1
14 return
lambda表达式
invokedynamic
invokedynamic用于调用动态方法,常用于lambda表达式。
package com.morris.jvm.methodinvoke;
public class InvokeDynamicDemo {
public static void main(String[] args) {
Runnable r = () -> System.out.println("hello");
}
}
main()方法对应的字节码如下:
0 invokedynamic #2 <run, BootstrapMethods #0>
5 astore_1
6 return
使用lambda表达式在编译时会在类中动态生成一个方法,该方法对应的字节码如下:
InnerClasses:
public static final #53= #52 of #56; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #26 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#27 ()V
#28 invokestatic com/morris/jvm/methodinvoke/InvokeDynamicDemo.lambda$main$0:()V
#27 ()V
BootstrapMethods属性在Java1.7以后才有,位于类文件的属性列表中,这个属性用于保存invokedynamic指令引用的引导方法限定符。和上面介绍的四个指令不同,invokedynamic并没有确切的接受对象,取而代之的,是一个叫CallSite的对象。
方法句柄(MethodHandle)
invokedynamic指令的底层,是使用方法句柄(MethodHandle)来实现的。方法句柄是一个能够被执行的引用,它可以指向静态方法和实例方法,以及虚构的get和set方法,从以下案例中可以看到MethodHandle提供的一些方法。
MethodHandle是什么?简单的说就是方法句柄,通过这个句柄可以调用相应的方法。用MethodHandle调用方法的流程为:
- 创建MethodType,获取指定方法的签名(出参和入参)
- 在Lookup中查找MethodType的方法句柄MethodHandle,可以通过相应的findxxx方法得到相应的MethodHandle,相当于MethodHandle的工厂方法。
- 传入方法参数通过MethodHandle调用方法
package com.morris.jvm.methodinvoke;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class MethodHandleDemo {
public static void main(String[] args) throws Throwable {
String hello = toString("hello");
System.out.println(hello);
String result = toString(1024);
System.out.println(result);
}
private static String toString(Object o) throws Throwable {
//方法类型表示接受的参数和返回类型(第一个参数是返回参数)
MethodType methodType = MethodType.methodType(String.class);
//方法句柄--工厂方法Factory
MethodHandles.Lookup lookup = MethodHandles.lookup();
//拿到具体的MethodHandle(findVirtual相当于字节码)
MethodHandle methodHandle = lookup.findVirtual(o.getClass(), "toString", methodType);
String obj = (String) methodHandle.invoke(o);
return obj;
}
}
分派
Java是一门面向对象的程序语言,因为Java具备面向对象的3个基本特征:继承、封装和多态。
分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在Java虚拟机之中是如何实现的?
静态分派
在编译期,根据方法涉及的引用类型(包括参数列表的引用类型和方法的调用者的引用类型)来确定方法调用的(初步)版本,并把相应的符号引用放在字节码指令中,这个步骤叫做静态分派。
先来看一下下面这段代码的执行结果:
package com.morris.jvm.methodinvoke;
public class StaticDispatch{
static abstract class Human{}
static class Man extends Human{ }
static class Woman extends Human{}
public void sayHello(Human guy){
System.out.println("hello,guy!");
}
public void sayHello(Man guy){
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy){
System.out.println("hello,lady!");
}
public static void main(String[]args){
StaticDispatch sr = new StaticDispatch();
Human man = new Man();
Human woman = new Woman();
sr.sayHello(man);
sr.sayHello(woman);
}
}
运行结果如下:
hello,guy!
hello,guy!
“Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type)。
静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
代码中定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。
静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
如果将代码改成下面这样,运行结果就不一样了:
Human human=new Man();
sr.sayHello((Man)human);
human=new Woman();
sr.sayHello((Woman)human);
运行结果如下:
hello,gentleman!
hello,lady!
动态分派
在运行时,根据对象的实际类型来确定方法的调用版本,这个步骤叫做动态分派。
package com.morris.jvm.methodinvoke;
public class Dispatch {
static class QQ{}
static class WX{}
public static class Father{
public void hardChoice(QQ arg){
System.out.println("father choose qq");
}
public void hardChoice(WX arg){
System.out.println("father choose weixin");
}
}
public static class Son extends Father{
public void hardChoice(QQ arg){
System.out.println("son choose qq");
}
public void hardChoice(WX arg){
System.out.println("son choose weixin");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new WX());
son.hardChoice(new QQ());
}
}
方法的重写也是使用invokevirtual指令,只是这个时候具备多态性。
invokevirtual指令有多态查找的机制,该指令运行时,解析过程如下:
- 找到操作数栈顶的第一个元素所指向的对象实际类型,记做c。
- 如果在类型c中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法直接引用,查找过程结束,不通过则返回java.lang.IllegalAccessError。
- 否则,按照继承关系从下往上依次对c的各个父类进行第二步的搜索和验证过程。
- 如果始终没找到合适的方法,则抛出java.lang.AbstractMethodError异常,这就是Java语言中方法重写的本质。
静态链接:在类加载过程中的解析阶段将符号引用转化为直接引用。
动态链接:在方法的运行过程中根据方法的参数类型将符号引用转化为直接引用。
动态分派会在JVM运行时会频繁的、反复的去搜索元数据,所以JVM使用了一种优化手段,就是在方法区中建立一个虚方法表,虚方法表示类信息的一种,在类加载过程中的准备阶段完成初始化,存放在方法区,使用虚方法表索引来替代元数据查找以提高性能。
子类中继承但未重写的方法,在子类的虚方法表中存放的入口地址,就是父类的虚方法表中的入口地址,指向父类的实现。在父子类中,相同符号引用的方法(重写)的方法,其在各自虚方法表中的索引相同。
相关推荐
- jQuery VS AngularJS 你更钟爱哪个?
-
在这一次的Web开发教程中,我会尽力解答有关于jQuery和AngularJS的两个非常常见的问题,即jQuery和AngularJS之间的区别是什么?也就是说jQueryVSAngularJS?...
- Jquery实时校验,指定长度的「负小数」,小数位未满末尾补0
-
在可以输入【负小数】的输入框获取到焦点时,移除千位分隔符,在输入数据时,实时校验输入内容是否正确,失去焦点后,添加千位分隔符格式化数字。同时小数位未满时末尾补0。HTML代码...
- 如何在pbootCMS前台调用自定义表单?pbootCMS自定义调用代码示例
-
要在pbootCMS前台调用自定义表单,您需要在后台创建表单并为其添加字段,然后在前台模板文件中添加相关代码,如提交按钮和表单验证代码。您还可以自定义表单数据的存储位置、添加文件上传字段、日期选择器、...
- 编程技巧:Jquery实时验证,指定长度的「负小数」
-
为了保障【负小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【负小数】的方法。HTML代码<inputtype="text"class="forc...
- 一篇文章带你用jquery mobile设计颜色拾取器
-
【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...
- 编程技巧:Jquery实时验证,指定长度的「正小数」
-
为了保障【正小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【正小数】的方法。HTML做成方法<inputtype="text"class="fo...
- jquery.validate检查数组全部验证
-
问题:html中有多个name[],每个参数都要进行验证是否为空,这个时候直接用required:true话,不能全部验证,只要这个数组中有一个有值就可以通过的。解决方法使用addmethod...
- Vue进阶(幺叁肆):npm查看包版本信息
-
第一种方式npmviewjqueryversions这种方式可以查看npm服务器上所有的...
- layui中使用lay-verify进行条件校验
-
一、layui的校验很简单,主要有以下步骤:1.在form表单内加上class="layui-form"2.在提交按钮上加上lay-submit3.在想要校验的标签,加上lay-...
- jQuery是什么?如何使用? jquery是什么功能组件
-
jQuery于2006年1月由JohnResig在BarCampNYC首次发布。它目前由TimmyWilson领导,并由一组开发人员维护。jQuery是一个JavaScript库,它简化了客户...
- django框架的表单form的理解和用法-9
-
表单呈现...
- jquery对上传文件的检测判断 jquery实现文件上传
-
总体思路:在前端使用jquery对上传文件做部分初步的判断,验证通过的文件利用ajaxFileUpload上传到服务器端,并将文件的存储路径保存到数据库。<asp:FileUploadI...
- Nodejs之MEAN栈开发(四)-- form验证及图片上传
-
这一节增加推荐图书的提交和删除功能,来学习node的form提交以及node的图片上传功能。开始之前需要源码同学可以先在git上fork:https://github.com/stoneniqiu/R...
- 大数据开发基础之JAVA jquery 大数据java实战
-
上一篇我们讲解了JAVAscript的基础知识、特点及基本语法以及组成及基本用途,本期就给大家带来了JAVAweb的第二个知识点jquery,大数据开发基础之JAVAjquery,这是本篇文章的主要...
- 推荐四个开源的jQuery可视化表单设计器
-
jquery开源在线表单拖拉设计器formBuilder(推荐)jQueryformBuilder是一个开源的WEB在线html表单设计器,开发人员可以通过拖拉实现一个可视化的表单。支持表单常用控件...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)