java符号的定位与解析(总结版)

java符号的定位与解析,从本质上来看,就是一个消除歧义的过程,这个过程从编译到运行,横跨了多个阶段,本文主要目的在于划分清楚哪个阶段消除了什么歧义

1.符号的起点-java代码

public class Parent {
  public void sayHi(){
  }
 public static void main(String[] args) {
    new Parent().sayHi("hi");
  }
}

那么从java语义上看,表达的意思大概就是:
1、有个public的Parent类
2、这个类有个类方法叫sayHi
3、类方法sayHi是个没有参数也没有返回值的public方法
4、main函数调用了sayHi函数
如同你画我猜一般,如何将这些意思精确的传递给jvm直到最终程序的运行呢

2.编译后-字节码

编译的过程就是将某一种语言编写的程序翻译成为一个等价的、用另一种语言编写的程序。那么对于java而言,编译就是将java代码翻译成字节码,编译的细节就不分析了,主要看编译的结果--字节码,以下是截取的部分字节码,分为两块,一块是方法表,一块是常量池

常量池中有一个常量指向sayHi函数:

Constant pool:
   #..."省略"
   #4 = Methodref          #2.#17         // Parent.sayHi:()V

Parent.sayHi:()V就是用来描述sayHi函数的符号引用,描述的很精确:类Parent的,没有入参的,返回void的,名为sayHi的方法
那么main方法里怎么调用这个sayHi()的呢,看方法表:

public static void main(java.lang.String[]);
 ...
    7: invokevirtual #4                  // Method sayHi:()V
 ...

invokevirtual可以暂时理解成调用,#4当然就是上文中常量池中的常量#4,它指向sayHi()方法,如此一个调用函数的过程就被字节码描述清楚了

从这个简单的例子可以得出结论:
字节码中的符号引用仅仅是一个描述性的符号(如:Parent.sayHi:()V),并没有保存最终的内存布局信息,这点与C语言的编译结果不一样

3.编译期间解决的歧义

3.1 重载

假如一个类中,void sayHi(String string)void sayHi(Object object)两个方法构成重载,当调用 sayHi("hi")时,看看字节码中的指令是什么:

invokevirtual #9                  // Method sayHi:(Ljava/lang/String;)V

显然,#9常量指向的是void sayHi(String string)方法,那么可以得出结论:
编译期间,重载造成的歧义就被解决了,在字节码中,已经选出应该调用的方法

继续看这个例子,调用方法时传入的参数为"hi",既是String类型,也是Object类型,但是在字节码中,直接认定了调用(Ljava/lang/String;)V,这是为什么呢,以下是《深入理解jvm》书中给出的解释:

Java 编译器选取重载方法的过程共分为三个阶段:
1、在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;
2、如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
3、如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。
如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。

3.2 遮蔽

下面这段代码,在init()函数中定义了一个与类变量同名的局部变量a

public class Parent5 {
  public String a = "out";
  public void init(){
       String a = "in";
       System.out.println(a);
       System.out.println(this.a);
  }
  public static void main(String[] args){
       new Parent5().init();
  }
}

直接看字节码中是如何获取a和this.a对应的值的:
常量池

Constant pool:
   #3 = Fieldref           #7.#24         // Parent5.a:Ljava/lang/String;
   #4 = String             #25            // in

获取a的值

0: ldc           #4                  // String in

从常量池的#4常量中获取值(指向了字符串"in"

获取this.a的值

getfield      #3                  // Field a:Ljava/lang/String;

从常量池的#3常量中获取值(指向了Parent5.a:Ljava/lang/String,即类变量a)
显然,字节码层面上已经对调用哪个变量区分的明明白白,那么可以得出结论:
java语言层面上定义的遮蔽,其在编译过程中就被实现

3.3 隐藏

隐藏的典型场景是对于一个静态方法,多态的特性失效了
java代码

public class Parent4 {
  public static void sayHi(){
    System.out.println("hi,son");
  }
}

class Son4 extends Parent4{
  public static void sayHi(){
      System.out.println("hi,parent");
   }

  public static void main(String[] args) {
    Parent4 son4 = new Son4();
    son4.sayHi();
  }
}

本例中,将打印出"hi,son",这与多态的场景基本一致,区别在于本例中的方法是静态方法,所以尽管真正的实例是Son4类型的,但最后还是调用的父类的方法

son4.sayHi();对应的字节码

10: invokestatic  #7                  // Method Parent4.sayHi:()V

上述指令中,#7常量指向的是父类方法(Method Parent4.sayHi:()V),且使用的是invokestatic指令,这条指令是专门用来调用静态方法的,具体invokestatic的实现逻辑可以看源码解析
简单总结一下就是: 在解析invokestatic #7时,直接将#7指向的符号引用解析成直接引用并返回,并不关心运行时实际对象的类型(即只有连接时解析,没有运行时解析,后文会进行介绍)

那么可以得出结论:
java语言层面上定义的隐藏,是在编译时消除的歧义

4.解析期间解决的歧义

上文讲到,在字节码中,调用方法时,指向的只是一个方法的符号引用,而不是最终的内存地址,那么就需要一个步骤将符号引用转为直接引用,这一步骤称为解析,以下是一些基本的名词解释

符号引用:是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可. 符号引用的目标不一定要加载到内存中.

直接引用:是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄. 如果有了直接引用,那引用的目标必定存在于内存中.

虚拟机规范中并没有明确规定解析阶段发生的具体时间,只要求了在执行 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic 用于操作符号引用的字节码指令前,先对它们所使用的符号进行解析. 所以虚拟机实现可以根据需要来判断到底是在类被加载时就对常量池中的符号进行解析,还是等到一个符号将要被使用前才解析它.

4.1 多态&重写

以一个多态的例子来分析解析的过程
java代码

class Son3 extends Parent3
...
Parent3 son3 = new Son3();
son3.sayHi();

son3.sayHi()对应的字节码

invokevirtual #7                  // Method Parent3.sayHi:()V

可以看出,#7常量指向的是父类方法(Method Parent3.sayHi:()V),显然在编译后,歧义没有消除,那么来看解析invokevirtual指令,可以将其分为两个步骤:连接时解解析运行时解析 (根据源码函数翻译而来)

4.1.1 连接时解析(linktime_resolve_virtual_method)

根据字节码中的符号引用Parent3.sayHi:()V,解析其对应的直接引用,具体寻找过程如下(源码可看另一篇文章):
1、遍历Parent3类的方法列表,根据符号引用来找到匹配的方法,并返回它的直接引用(methodHandle)
2、如果第一步找不到,就去Parent3的父类中找,还找不到,就去Parent3实现的接口中找

4.1.2 运行时解析(runtime_resolve_virtual_method)

连接时解析已经获得了Parent3.sayHi:()V对应的直接引用methodHandle,接下来的步骤是:
1、根据methodHandle,获取sayHi()函数在Parent3类对应虚方法表中的位置vtable_index
2、jvm在运行时可以获取到实际对象的类型,即Son3类,获取其对应的虚表
3、从Son3的虚方法表中获取vtable_index位置指向的直接引用

解析符号引用.png
invokevirtual #7                  // Method Parent3.sayHi:()V

解析前,常量#7指向Parent3.sayHi:()V的符号引用
解析后,常量#7指向Son3::sayHi()V的直接引用
这就是多态的实现,在运行时解析消除了歧义
解析的过程中设计到了虚方法表的概念,下面会进行介绍

4.1.3 虚方法表

虚方法表(简称虚表)的具体初始化细节和源码可以看另一篇文章,这里总结一些相关的概念:
1、虚表是一种用于实现dynamic dispatch机制(或者说runtime method binding机制,也就是我们说的多态)的工具,C++也有用到
2、每个类对应一个虚表,虚表中不存放静态方法和私有方法(只为invoke_virtual服务)
3、虚表中,也会存放父类的方法,且对于同一个函数,在父类和子类的虚表中的位置是一样的,如果没有重写,二者指向同一个引用,若发生重写,则不同

举个简单的例子,Son3重写了Parent3的sayHi方法,以下是它们对应的虚表


虚方法表.png

5. 总结

1、编译期间消除了重载、遮蔽、隐藏这三种情况的歧义
2、解析是符号引用转化成直接引用的过程
3、解析可分为连接时解析和运行时解析两步,静态方法的解析没有第二步
4、多态&重写则是在解析时(通过虚表)消除的歧义

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容