> Java > java지도 시간 > Java 동적 바인딩 메커니즘의 내부 이야기에 대한 자세한 설명(그림)

Java 동적 바인딩 메커니즘의 내부 이야기에 대한 자세한 설명(그림)

黄舟
풀어 주다: 2017-03-20 10:27:30
원래의
1347명이 탐색했습니다.

Java 메소드를 호출하는 과정에서 JVM은 어떤 클래스의 메소드 소스 코드가 호출되는지 어떻게 알 수 있나요? 여기에 대한 내부 이야기는 무엇입니까? 이번 글에서는 JVM 메소드 호출의 정적(정적 바인딩)과 동적 바인딩 메커니즘(자동 바인딩)을 공개하겠습니다.

정적 바인딩 메커니즘

//被调用的类
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번째 상수 테이블의 인덱스 항목을 참조합니다(상수 풀에 대한 자세한 내용은 , "클래스 파일 내용 및 상수 풀 참조 》). 이 상수 테이블(CONSTATN_Methodref_info)은 메소드 f1 정보(클래스 이름, 메소드 이름 및 f1의 반환 유형 포함)의 기호 참조를 기록합니다. JVM은 먼저 f1 메소드가 다음 기호 참조를 기반으로 하는 클래스의 정규화된 이름을 찾습니다.

(2) 그런 다음 JVM은 Father 클래스를 로드하고 연결하고 초기화합니다. ;

(3) 그런 다음 Father 클래스가 있는 메서드 영역에서 f1() 메서드의 직접 주소를 찾고 이 직접 주소를 상수의 인덱스 13이 있는 상수 테이블에 기록합니다. StaticCall 클래스의 풀. 이 프로세스를 상수 풀 구문 분석이라고 합니다. 나중에 Father.f1()이 다시 호출되면 f1 메서드의 바이트 코드를 직접 찾을 수 있습니다.

(4) StaticCall 클래스 상수 완성 풀 인덱스 항목 13의 상수 테이블이 구문 분석된 후 JVM은 f1() 메서드를 호출하고 f1() 메서드의 명령을 해석하고 실행하기 시작할 수 있습니다.

위 과정을 통해 JVM은 상수 풀을 파싱한 후 호출할 f1() 메서드가 메모리 내 어디에 위치하는지 확인할 수 있음을 발견했습니다. 실제로 이 정보는 컴파일 단계에서 StaticCall 클래스의 상수 풀에 기록되었습니다. 컴파일 단계에서 어떤 메서드를 호출할지 결정하는 이러한 방식을 정적 바인딩 메커니즘이라고 합니다.

정적으로 수정된 정적 메서드를 제외하고, 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()
	}
}
로그인 후 복사

위 소스 코드에는 세 가지 중요한 개념이 있습니다: 다형성, 메서드 재정의, 메소드 오버로딩 . 인쇄된 결과는 모든 사람에게 분명하지만 f.f1()이 Father의 메서드 대신 Sun 하위 클래스의 메서드를 호출한다는 것을 JVM이 어떻게 알 수 있습니까? 이 문제를 설명하기 전에 먼저 JVM이 관리하는 매우 중요한 데이터 구조인 메서드 테이블에 대해 간략하게 살펴보겠습니다.

JVM이 클래스를 로드할 때 메소드 영역에 이 클래스에 대한 많은 정보를 저장합니다(자세한 내용은 "Java Virtual Machine Architecture" 참조). 》). 메소드 테이블이라는 데이터 구조가 있습니다. 현재 클래스와 모든 상위클래스의 표시 메소드 바이트코드의 메모리에 직접 주소를 배열 형식으로 기록합니다. 다음 그림은 위 소스코드에서 Father, Sun 클래스의 메소드 영역에 있는 메소드 테이블이다.

위 그림의 메소드 테이블은 두 가지 특징을 가지고 있다. : (1) 하위 클래스 메서드 테이블에서 는 Father가 Object를 확장하는 것과 같이 상위 클래스에서 의 메서드를 상속합니다. (2) 동일한 메소드(동일 메소드 시그니처: 메소드 이름 및 매개변수 목록)는 모든 클래스의 메소드 테이블에서 동일한 인덱스를 갖습니다. 예를 들어, Father 메소드 테이블의 f1()과 Son 메소드 테이블의 f1()은 모두 해당 메소드 테이블의 11번째 항목에 위치합니다.

위 소스 코드의 경우 컴파일러는 먼저 기본 메소드를 다음 바이트코드 명령어로 컴파일합니다.

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은 먼저 이 기호 참조인 hr.test.Father를 기반으로 f1 메소드를 호출하는 클래스의 완전한 이름을 찾습니다. 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对象所属类型的方法表。过程如下图所示:

(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!”);
      }
}
로그인 후 복사

위 내용은 Java 동적 바인딩 메커니즘의 내부 이야기에 대한 자세한 설명(그림)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿