java 动态绑定原理_详解Java动态绑定机制的内幕(图)

在Java方法调用的过程中,JVM是如何知道调用的是哪个类的方法源代码? 这里面到底有什么内幕呢? 这篇文章我们就将揭露JVM方法调用的静态(static binding) 和动态绑定机制(auto binding) 。

静态绑定机制//被调用的类

package hr.test;

class Father{

public static void f1(){

System.out.println("Father— f1()");

}

}

//调用静态方法

import hr.test.Father;

public class StaticCall{

public static void main(){

Father.f1(); //调用静态方法

}

}

上面的源代码中执行方法调用的语句(Father.f1())被编译器编译成了一条指令:invokestatic #13。我们看看JVM是如何处理这条指令的

(1) 指令中的#13指的是StaticCall类的常量池中第13个常量表的索引项(关于常量池详见《Class文件内容及常量池

》)。这个常量表(CONSTATN_Methodref_info) 记录的是方法f1信息的符号引用(包括f1所在的类名,方法名和返回类型)。JVM会首先根据这个符号引用找到方法f1所在的类的全限定名: hr.test.Father;

(2) 紧接着JVM会加载、链接和初始化Father类;

(3) 然后在Father类所在的方法区中找到f1()方法的直接地址,并将这个直接地址记录到StaticCall类的常量池索引为13的常量表中。这个过程叫常量池解析,以后再次调用Father.f1()时,将直接找到f1方法的字节码;

(4) 完成了StaticCall类常量池索引项13的常量表的解析之后,JVM就可以调用f1()方法,并开始解释执行f1()方法中的指令了。

通过上面的过程,我们发现经过常量池解析之后,JVM就能够确定要调用的f1()方法具体在内存的什么位置上了。实际上,这个信息在编译阶段就已经在StaticCall类的常量池中记录了下来。这种在编译阶段就能够确定调用哪个方法的方式,我们叫做静态绑定机制。

除了被static 修饰的静态方法,所有被private 修饰的私有方法、被final 修饰的禁止子类覆盖的方法都会被编译成invokestatic指令。另外所有类的初始化方法和会被编译成invokespecial指令。JVM会采用静态绑定机制来顺利的调用这些方法。

动态绑定机制package hr.test;

//被调用的父类

class Father{

public void f1(){

System.out.println("father-f1()");

}

public void f1(int i){

System.out.println("father-f1() para-int "+i);

}

}

//被调用的子类

class Son extends Father{

public void f1(){ //覆盖父类的方法

System.out.println("Son-f1()");

}

public void f1(char c){

System.out.println("Son-s1() para-char "+c);

}

}

//调用方法

import hr.test.*;

public class AutoCall{

public static void main(String[] args){

Father father=new Son(); //多态

father.f1(); //打印结果: Son-f1()

}

}

上面的源代码中有三个重要的概念:多态(polymorphism)、方法覆盖 、方法重载。打印的结果大家也都比较清楚,但是JVM是如何知道f.f1()调用的是子类Sun中方法而不是Father中的方法呢?在解释这个问题之前,我们首先简单的讲下JVM管理的一个非常重要的数据结构——方法表。

在JVM加载类的同时,会在方法区中为这个类存放很多信息(详见《Java 虚拟机体系结构

》)。其中就有一个数据结构叫方法表。它以数组的形式记录了当前类及其所有超类的可见方法字节码在内存中的直接地址。下图是上面源代码中Father和Sun类在方法区中的方法表:

5e6d9bc9a1dbcc1ace963a04fc48f0dd.png

上图中的方法表有两个特点:(1) 子类方法表中继承了父类的方法,比如Father extends Object。 (2) 相同的方法(相同的方法签名:方法名和参数列表)在所有类的方法表中的索引相同。比如Father方法表中的f1()和Son方法表中的f1()都位于各自方法表的第11项中。

对于上面的源代码,编译器首先会把main方法编译成下面的字节码指令:0 new hr.test.Son [13] //在堆中开辟一个Son对象的内存空间,并将对象引用压入操作数栈

3 dup

4 invokespecial #7 [15] // 调用初始化方法来初始化堆中的Son对象

7 astore_1 //弹出操作数栈的Son对象引用压入局部变量1中

8 aload_1 //取出局部变量1中的对象引用压入操作数栈

9 invokevirtual #15 //调用f1()方法

12 return

其中invokevirtual指令的详细调用过程是这样的:

(1) invokevirtual指令中的#15指的是AutoCall类的常量池中第15个常量表的索引项。这个常量表(CONSTATN_Methodref_info) 记录的是方法f1信息的符号引用(包括f1所在的类名,方法名和返回类型)。JVM会首先根据这个符号引用找到调用方法f1的类的全限定名: hr.test.Father。这是因为调用方法f1的类的对象father声明为Father类型。

(2) 在Father类型的方法表中查找方法f1,如果找到,则将方法f1在方法表中的索引项11(如上图)记录到AutoCall类的常量池中第15个常量表中(常量池解析)。这里有一点要注意:如果Father类型方法表中没有方法f1,那么即使Son类型中方法表有,编译的时候也通过不了。因为调用方法f1的类的对象father的声明为Father类型。

(3) 在调用invokevirtual指令前有一个aload_1指令,它会将开始创建在堆中的Son对象的引用压入操作数栈。然后invokevirtual指令会根据这个Son对象的引用首先找到堆中的Son对象,然后进一步找到Son对象所属类型的方法表。过程如下图所示:

c2107a55c9cf924ed25a9a669d4e72e3.png

(4) 这是通过第(2)步中解析完成的#15常量表中的方法表的索引项11,可以定位到Son类型方法表中的方法f1(),然后通过直接地址找到该方法字节码所在的内存空间。

很明显,根据对象(father)的声明类型(Father)还不能够确定调用方法f1的位置,必须根据father在堆中实际创建的对象类型Son来确定f1方法所在的位置。这种在程序运行过程中,通过动态创建的对象的方法表来定位方法的方式,我们叫做 动态绑定机制。

上面的过程很清楚的反映出在方法覆盖的多态调用的情况下,JVM是如何定位到准确的方法的。但是下面的调用方法JVM是如何定位的呢?(仍然使用上面代码中的Father和Son类型)public class AutoCall{

public static void main(String[] args){

Father father=new Son();

char c='a';

father.f1(c); //打印结果:father-f1() para-int 97

}

}

问题是Fahter类型中并没有方法签名为f1(char)的方法呀。但打印结果显示JVM调用了Father类型中的f1(int)方法,并没有调用到Son类型中的f1(char)方法。

根据上面详细阐述的调用过程,首先可以明确的是:JVM首先是根据对象father声明的类型Father来解析常量池的(也就是用Father方法表中的索引项来代替常量池中的符号引用)。如果Father中没有匹配到”合适” 的方法,就无法进行常量池解析,这在编译阶段就通过不了。

那么什么叫”合适”的方法呢?当然,方法签名完全一样的方法自然是合适的。但是如果方法中的参数类型在声明的类型中并不能找到呢?比如上面的代码中调用father.f1(char),Father类型并没有f1(char)的方法签名。实际上,JVM会找到一种“凑合”的办法,就是通过 参数的自动转型来找 到“合适”的 方法。比如char可以通过自动转型成int,那么Father类中就可以匹配到这个方法了 (关于Java的自动转型问题可以参见《【解惑】Java类型间的转型》)。但是还有一个问题,如果通过自动转型发现可以“凑合”出两个方法的话怎么办?比如下面的代码:class Father{

public void f1(Object o){

System.out.println("Object");

}

public void f1(double[] d){

System.out.println("double[]");

}

}

public class Demo{

public static void main(String[] args) {

new Father().f1(null); //打印结果: double[]

}

}

null可以引用于任何的引用类型,那么JVM如何确定“合适”的方法呢。一个很重要的标准就是:如果一个方法可以接受传递给另一个方法的任何参数,那么第一个方法就相对不合适。比如上面的代码: 任何传递给f1(double[])方法的参数都可以传递给f1(Object)方法,而反之却不行,那么f1(double[])方法就更合适。因此JVM就会调用这个更合适的方法。

总结(1) 所有私有方法、静态方法、构造器及初始化方法都是采用静态绑定机制。在编译器阶段就已经指明了调用方法在常量池中的符号引用,JVM运行的时候只需要进行一次常量池解析即可。

(2) 类对象方法的调用必须在运行过程中采用动态绑定机制。

首先,根据对象的声明类型(对象引用的类型)找到“合适”的方法。具体步骤如下:

① 如果能在声明类型中匹配到方法签名完全一样(参数类型一致)的方法,那么这个方法是最合适的。

② 在第①条不能满足的情况下,寻找可以“凑合”的方法。标准就是通过将参数类型进行自动转型之后再进行匹配。如果匹配到多个自动转型后的方法签名f(A)和f(B),则用下面的标准来确定合适的方法:传递给f(A)方法的参数都可以传递给f(B),则f(A)最合适。反之f(B)最合适 。

③ 如果仍然在声明类型中找不到“合适”的方法,则编译阶段就无法通过。

然后,根据在堆中创建对象的实际类型找到对应的方法表,从中确定具体的方法在内存中的位置。

覆写(override)

一个实例方法可以覆写(override)在其超类中可访问到的具有相同签名的所有实例方法,从而使能了动态分派(dynamic dispatch);换句话说,VM将基于实例的运行期类型来选择要调用的覆写方法。覆写是面向对象编程技术的基础,并且是唯一没有被普遍劝阻的名字重用形式:class Base{

public void f(){}

}

class Derived extends Base{

public void f(){}

}

隐藏(hide)

一个域、静态方法或成员类型可以分别隐藏(hide)在其超类中可访问到的具有相同名字(对方法而言就是相同的方法签名)的所有域、静态方法或成员类型。隐藏一个成员将阻止其被继承。class Base{

public static void f(){}

}

class Derived extends Base {

private static void f(){} //hides Base. f()

}

重载(overload)

在某个类中的方法可以重载(overload)另一个方法,只要它们具有相同的名字和不同的签名。由调用所指定的重载方法是在编译期选定的。class CircuitBreaker{

public void f (int i){} //int overloading

public void f(String s){} //String overloading

}

遮蔽(shadow)

一个变量、方法或类型可以分别遮蔽(shadow)在一个闭合的文本范围内的具有相同名字的所有变量、方法或类型。如果一个实体被遮蔽了,那么你用它的简单名是无法引用到它的;根据实体的不同,有时你根本就无法引用到它。class WhoKnows{

static String sentence=”I don't know.”;

public static void main(String[] args〕{

String sentence=”I don't know.”; //shadows static field

System.out. println (sentence); // prints local variable

}

}

尽管遮蔽通常是被劝阻的,但是有一种通用的惯用法确实涉及遮蔽。构造器经常将来自其所在类的某个域名重用为一个参数,以传递这个命名域的值。这种惯用法并不是没有风险,但是大多数Java程序员都认为这种风格带来的实惠要超过

其风险:class Belt{

private find int size ; //Parameter shadows Belt. size

public Belt (int size){

this. size=size;

}

}

遮掩(obscure)

一个变量可以遮掩具有相同名字的一个类型,只要它们都在同一个范围内:如果这个名字被用于变量与类型都被许可的范围,那么它将引用到变量上。相似地,一个变量或一个类型可以遮掩一个包。遮掩是唯一一种两个名字位于不同的名字空间的名字重用形式,这些名字空间包括:变量、包、方法或类型。如果一个类型或一个包被遮掩了,那么你不能通过其简单名引用到它,除非是在这样一个上下文环境中,即语法只允许在其名字空间中出现一种名字。遵守命名习惯就可以极大地消除产生遮掩的可能性:public class Obscure{

static String System;// Obscures type java.lang.System

public static void main(String[] args)

// Next line won't compile:System refers to static field

System. out. println(“hello, obscure world!”);

}

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/244739.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python重新加载模块_jupyter实现重新加载模块

最近几年,jupyter在全球数据科学领域,已经成为不可或缺的重要工具。在jupyter中用python写程序,若import了自己写的外部模块,如果这个外部模块有更新,再次执行import,jupyter是不会重新导入的。一般的做法是…

java连接access2013数据库_滴水穿石–Java连接Access数据库及其操作

1、配置数据源【控制面板】—>【管理工具】—>【数据源ODBC】点击添加选择Microsoft Access Driver填写数据源名(自定义,如test),并选择数据库(指定你的Access数据库文件),如下图红色箭头标注最后,点击确定数据源配置完成2、…

python抠透明图_python利用蒙版抠图(使用PIL.Image和cv2)输出透明背景图

因为最近在做深度学习抠图,正好要用到蒙版进行抠图,所以我将抠图代码进行了封装注释,可以直接使用。可能走了弯路,若有高见请一定提出!主要代码import cv2from PIL import Imageimport numpy as npclass UnsupportedFo…

java退出不报异常_如何优雅的处理异常(java)?

这篇文章应该可以解答你的疑问。Java中异常提供了一种识别及响应错误情况的一致性机制,有效地异常处理能使程序更加健壮、易于调试。异常之所以是一种强大的调试手段,在于其回答了以下三个问题:什么出了错?在哪出的错?为什么出错?在有效使…

无法复制winevt中的文件_u盘文件无法复制怎么解决 u盘文件无法复制解决方法【详细步骤】...

在使用u盘的过程中有时会出现 文件无法复制 的问题,并且会弹出磁盘已满的提示,而经过检查,u盘容量却拥有足够大的空间,那么此时该如何解决呢?接下来就跟着小编学习如何解决 u盘文件无法复制 的问题。u盘文件无法复制并提示磁盘已…

python 接口测试 url_Python 接口测试之接口请求方法封装

引言前面讲过三篇文章:既然我们接口测试用例写好了,测试数据也拿到了,那么就是模拟调用接口的方法了,方法有get,post,put,delete,具体是选择哪种,我们需要根据不同接口规定好请求方法来调用。为了方便&…

jenkins 项目启动日志_jenkins 修改启动文件即jdk路径、log路径和切换jenkins用户

环境操作系统: win7服务器(虚拟机):centos6.5(7)工具:CRT提前条件已经安装好了jenkins修改(或者说添加)jdk路径sudo vim /etc/init.d/jenkins# jenkins的版本是2.8candidates" /usr/programe_files/jdk1.8.0_121/bin/java /etc/alternat…

python3远程连接_Python3 SSH远程连接服务器的方法示例

下载paramiko首先,我的windows系统上有python2和python3。使用下面命令切换到python3:activate py3接着使用下面命令下载相关模块:pip install ecdsapip install Cryptopip install paramiko连接服务器操作:# -*- coding: utf-8 -*-import pa…

java jar 环境变量_java-jar jar包带环境变量(参数)启动

需求java工程我们可以编译成jar也可以翻译成war,一般地,war包我会丢到tomcat容器里,启动tomcat来访问服务,端口、SSL证书、日志等等,都托给tomcat。如果打的是jar包,我通常会用nohup启动,比如生…

java单列_Java 单例模式

单例模式保证一个类只有一个实例,并且提供一个访问该实例的全局访问点常见的应用场景:任务管理器。回收站。网站的计数器。操作系统的文件系统。在servlet编程中,每个servlet也是单例模式,项目中,读取配置文件的类&…

python字符串索引必须是整数_TypeError:在Python中,字符串索引必须是整数

我想从currency converter API得到的Json响应中获取key和value,得到一个错误:“of string index must be integers”。下面是Python代码、Json中的数据和错误消息。在import jsonfrom urllib.request import urlopenwith urlopen ("http://free.cur…

java 数字图片识别_java – 识别图像中的数字

您很可能需要执行以下操作:>在整个页面上应用Hough Transform算法,这应该会产生一系列页面部分。>对于您获得的每个部分,请再次应用。如果当前部分产生2个元素,那么你应该处理类似于上面的矩形。>完成后,您可…

python的基础命令_Python必学之命令行常用的命令

打开命令行,我们会看到每行前面都有诸如C:\Documents and Settings\Crossin>或者MyMacBook:~ crossin$之类的。这个提示符表示了当前命令行所在目录。在这里,我们输入python就可以进入python环境了。但今天我们暂时不这么做。第一个常用的命令是&…

内存泄漏java例子_一次线上Java应用内存泄漏分析实例

由于JVM的内存管理采用GC垃圾自动回收机制,这使得Java程序员在编程的时候确实可以从内存管理中释放出来,但这也引发了另外一个大问题,一旦Java应用出现内存泄漏的时候,常常让人措手不及,陷入无从下手的尴尬境地&#x…

python比较列表所有项是否有相同的部分_检查列表中的所有元素是否相同

一般方法&#xff1a;def checkEqual1(iterator):iterator iter(iterator)try:first next(iterator)except StopIteration:return Truereturn all(first rest for rest in iterator)单线&#xff1a;def checkEqual2(iterator):return len(set(iterator)) < 1还有一条线&…

java项目管理工具_Java 开发项目管理工具

团队协同工具(worktil):https://worktile.com/版本管理工具(GitLab)&#xff1a;http://www.tuicool.com/articles/bEz6Vf 配置教程&#xff1a;http://www.cnblogs.com/wintersun/p/3930900.html自动化构建工具(jenkins)&#xff1a;http://jenkins-ci.org/ 配置教程&#xff…

python判断是不是整数的命令_介绍python判断一个数是不是正小数和整数的方法

这篇文章主要介绍了python 判断是否为正小数和正整数的实例的相关资料,这里提供实例&#xff0c;实例注释说明很清楚&#xff0c;需要的朋友可以参考下python 判断是否为正小数和正整数的实例实现代码&#xff1a;def check_float(string):#支付时&#xff0c;输入的金额可能是…

.net core 2.1 mysql_ASP.NET Core 2.2 + MySQL + DB First

1 项目添加 Pomelo.EntityFrameworkCore.MySql2 编辑项目文件&#xff0c;在节点中添加如下代码&#xff1a;netcoreapp2.2InProcess3 CMD 切换到当前项目目录&#xff0c;执行如下命令dotnet ef dbcontext scaffold "Serverlocalhost;User Id数据库访问用户;Password数据…

java 向上抛异常_java throws 向上抛出的概念问题

展开全部------------------附注------------------------向上抛出的意思 针对 子类 父类,这里面涉及到几个方面,最重32313133353236313431303231363533e4b893e5b19e31333332613637要的一点,(先抛子类的异常,再抛父类的异常.)(FALSE)如果在该方法写明catch子句,catch的顺序是子…

windows linux cpu 抢占式 时间片_嵌入式Linux中进程调度怎样来解析

合作微信&#xff1a;xydf321456Linux是一套免费使用和自由传播的类Unix操作系统&#xff0c;是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。1.前言处理机&#xff08;CPU&#xff09;是整个计算机系…