java 符号引用与直接引用

摘要:
运行一次后,符号引用将替换为直接引用,下次不需要搜索。直接引用是偏移。通过偏移量,虚拟机可以直接找到该类内存区域中方法字节码的起始位置。可以看到,类文件中invokevirtual指令的操作数在经过几层间接寻址后都由字符串表示。这是Class文件中“符号引用”的真实状态:具有类型/结构的字符串。
简单来说:
符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。

当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
转:
作者:RednaxelaFX
链接:http://www.zhihu.com/question/30300585/answer/51335493
来源:知乎

先看Class文件里的“符号引用”。

考虑这样一个Java类:
public class X {
  public void foo() {
    bar();
  }

  public void bar() { }
}
它编译出来的Class文件的文本表现形式如下:
Classfile /private/tmp/X.class
  Last modified Jun 13, 2015; size 372 bytes
  MD5 checksum 8abb9cbb66266e8bc3f5eeb35c3cc4dd
  Compiled from "X.java"
public class X
  SourceFile: "X.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#16         //  java/lang/Object."<init>":()V
   #2 = Methodref          #3.#17         //  X.bar:()V
   #3 = Class              #18            //  X
   #4 = Class              #19            //  java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               LX;
  #12 = Utf8               foo
  #13 = Utf8               bar
  #14 = Utf8               SourceFile
  #15 = Utf8               X.java
  #16 = NameAndType        #5:#6          //  "<init>":()V
  #17 = NameAndType        #13:#6         //  bar:()V
  #18 = Utf8               X
  #19 = Utf8               java/lang/Object
{
  public X();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   LX;

  public void foo();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokevirtual #2                  // Method bar:()V
         4: return        
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   LX;

  public void bar();
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return        
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       1     0  this   LX;
}
可以看到Class文件里有一段叫做“常量池”,里面存储的该Class文件里的大部分常量的内容。

来考察foo()方法里的一条字节码指令:
1: invokevirtual #2  // Method bar:()V
这在Class文件中的实际编码为:
[B6] [00 02]
其中0xB6是invokevirtual指令的操作码(opcode),后面的0x0002是该指令的操作数(operand),用于指定要调用的目标方法。
这个参数是Class文件里的常量池的下标。那么去找下标为2的常量池项,是:
#2 = Methodref          #3.#17         //  X.bar:()V
这在Class文件中的实际编码为(以十六进制表示,Class文件里使用高位在前字节序(big-endian)):
[0A] [00 03] [00 11]
其中0x0A是CONSTANT_Methodref_info的tag,后面的0x0003和0x0011是该常量池项的两个部分:class_index和name_and_type_index。这两部分分别都是常量池下标,引用着另外两个常量池项。
顺着这条线索把能传递引用到的常量池项都找出来,会看到(按深度优先顺序排列):
   #2 = Methodref          #3.#17         //  X.bar:()V
   #3 = Class              #18            //  X
  #18 = Utf8               X
  #17 = NameAndType        #13:#6         //  bar:()V
  #13 = Utf8               bar
   #6 = Utf8               ()V
把引用关系画成一棵树的话:
     #2 Methodref X.bar:()V
     /                     
#3 Class X       #17 NameAndType bar:()V
    |                /             
#18 Utf8 X    #13 Utf8 bar     #6 Utf8 ()V

标记为Utf8的常量池项在Class文件中实际为CONSTANT_Utf8_info,是以略微修改过的UTF-8编码的字符串文本。

这样就清楚了对不对?
由此可以看出,Class文件中的invokevirtual指令的操作数经过几层间接之后,最后都是由字符串来表示的。这就是Class文件里的“符号引用”的实态:带有类型(tag) / 结构(符号间引用层次)的字符串。

==================================================

然后再看JVM里的“直接引用”的样子。

这里就不拿HotSpot VM来举例了,因为它的实现略复杂。让我们看个更简单的实现,Sun的元祖JVM——Sun JDK 1.0.2的32位x86上的做法。
请先参考另一个回答里讲到Sun Classic VM的部分:为什么bs虚函数表的地址(int*)(&bs)与虚函数地址(int*)*(int*)(&bs) 不是同一个? - RednaxelaFX 的回答

Sun Classic VM:(以32位Sun JDK 1.0.2在x86上为例)

         HObject             ClassObject
                       -4 [ hdr            ]
--> +0 [ obj     ] --> +0 [ ... fields ... ]
    +4 [ methods ] 
                             methodtable            ClassClass
                     > +0  [ classdescriptor ] --> +0 [ ... ]
                       +4  [ vtable[0]       ]      methodblock
                       +8  [ vtable[1]       ] --> +0 [ ... ]
                       ... [ vtable...       ]
(请留心阅读上面链接里关于虚方法表与JVM的部分。Sun的元祖JVM也是用虚方法表的喔。)

元祖JVM在做类加载的时候会把Class文件的各个部分分别解析(parse)为JVM的内部数据结构。例如说类的元数据记录在ClassClass结构体里,每个方法的元数据记录在各自的methodblock结构体里,等等。
在刚加载好一个类的时候,Class文件里的常量池和每个方法的字节码(Code属性)会被基本原样的拷贝到内存里先放着,也就是说仍然处于使用“符号引用”的状态;直到真的要被使用到的时候才会被解析(resolve)为直接引用。

假定我们要第一次执行到foo()方法里调用bar()方法的那条invokevirtual指令了。
此时JVM会发现该指令尚未被解析(resolve),所以会先去解析一下。
通过其操作数所记录的常量池下标0x0002,找到常量池项#2,发现该常量池项也尚未被解析(resolve),于是进一步去解析一下。
通过Methodref所记录的class_index找到类名,进一步找到被调用方法的类的ClassClass结构体;然后通过name_and_type_index找到方法名和方法描述符,到ClassClass结构体上记录的方法列表里找到匹配的那个methodblock;最终把找到的methodblock的指针写回到常量池项#2里。

也就是说,原本常量池项#2在类加载后的运行时常量池里的内容跟Class文件里的一致,是:
[00 03] [00 11]
(tag被放到了别的地方;小细节:刚加载进来的时候数据仍然是按高位在前字节序存储的)
而在解析后,假设找到的methodblock*是0x45762300,那么常量池项#2的内容会变为:
[00 23 76 45]
(解析后字节序使用x86原生使用的低位在前字节序(little-endian),为了后续使用方便)
这样,以后再查询到常量池项#2时,里面就不再是一个符号引用,而是一个能直接找到Java方法元数据的methodblock*了。这里的methodblock*就是一个“直接引用”

解析好常量池项#2之后回到invokevirtual指令的解析。
回顾一下,在解析前那条指令的内容是:
[B6] [00 02]
而在解析后,这块代码被改写为:
[D6] [06] [01]
其中opcode部分从invokevirtual改写为invokevirtual_quick,以表示该指令已经解析完毕。
原本存储操作数的2字节空间现在分别存了2个1字节信息,第一个是虚方法表的下标(vtable index),第二个是方法的参数个数。这两项信息都由前面解析常量池项#2得到的methodblock*读取而来。
也就是:
invokevirtual_quick vtable_index=6, args_size=1

这里例子里,类X对应在JVM里的虚方法表会是这个样子的:
[0]: java.lang.Object.hashCode:()I
[1]: java.lang.Object.equals:(Ljava/lang/Object;)Z
[2]: java.lang.Object.clone:()Ljava/lang/Object;
[3]: java.lang.Object.toString:()Ljava/lang/String;
[4]: java.lang.Object.finalize:()V
[5]: X.foo:()V
[6]: X.bar:()V
所以JVM在执行invokevirtual_quick要调用X.bar()时,只要顺着对象引用查找到虚方法表,然后从中取出第6项的methodblock*,就可以找到实际应该调用的目标然后调用过去了。

假如类X还有子类Y,并且Y覆写了bar()方法,那么类Y的虚方法表就会像这样:
[0]: java.lang.Object.hashCode:()I
[1]: java.lang.Object.equals:(Ljava/lang/Object;)Z
[2]: java.lang.Object.clone:()Ljava/lang/Object;
[3]: java.lang.Object.toString:()Ljava/lang/String;
[4]: java.lang.Object.finalize:()V
[5]: X.foo:()V
[6]: Y.bar:()V
于是通过vtable_index=6就可以找到类Y所实现的bar()方法。

所以说在解析/改写后的invokevirtual_quick指令里,虚方法表下标(vtable index)也是一个“直接引用”的表现。

关于这种“_quick”指令的设计,可以参考远古的JVM规范第1版的第9章。这里有一份拷贝:http://www.cs.miami.edu/~burt/reference/java/language_vm_specification.pdf

在现在的HotSpot VM里,围绕常量池、invokevirtual的解析(再次强调是resolve)的具体实现方式跟元祖JVM不一样,但是大体的思路还是相通的。

HotSpot VM的运行时常量池有ConstantPool和ConstantPoolCache两部分,有些类型的常量池项会直接在ConstantPool里解析,另一些会把解析的结果放到ConstantPoolCache里。以前发过一帖有简易的图解例子,可以参考:请问,jvm实现读取class文件常量池信息是怎样呢?

==================================================

由此可见,符号引用通常是设计字符串的——用文本形式来表示引用关系。

而直接引用是JVM(或其它运行时环境)所能直接使用的形式。它既可以表现为直接指针(如上面常量池项#2解析为methodblock*),也可能是其它形式(例如invokevirtual_quick指令里的vtable index)。
关键点不在于形式是否为“直接指针”,而是在于JVM是否能“直接使用”这种形式的数据。

免责声明:文章转载自《java 符号引用与直接引用》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Windows 无人值守安装应答文件详解UI Automator Viewer的使用下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

【ECMAScript5】ES5基础

一、语法 区分大小写。 变量是弱类型的,可以初始化为任意值,也可以随时改变变量所存数据的类型。 每行结尾的分号可有可无,但是建议加上。 注释 单行注释以双斜杠开头(//) 多行注释以单斜杠和星号开头(/*),以星号和单斜杠结尾(*/) 代码块:用{ } 包起来的 二、变量 使用 var (variable的缩写)运算符声明变量。 可以用一个var...

从深圳回武汉的面试感想以及一些面试题

  转载:https://www.cnblogs.com/itdragon/p/9026994.html 大家好,我是ITDragon龙,今天分享一些面试中常问的题目,和一些面试感受。我是四月二十五号离职,二十六号上午八点从深圳出发,下午三点到武汉。二十七号便开始参加面试。每天的节奏基本是:上午去公司面试,中午去小区房子装修监工,下午再去公司面试,中途还...

Linux下修改MySQL数据库字符编码为UTF-8解决中文乱码

由于MySQL编码原因会导致数据库出现乱码。 解决办法: 修改MySQL数据库字符编码为UTF-8,UTF-8包含全世界所有国家需要用到的字符,是国际编码。 具体操作: 1、进入MySQL控制台 >mysql -uroot -p #输入密码进入 >status; #查看当前MySQL运行状态,如下图所示:  ...

python读取文件时出现多余的ufeff时的解决方法及原因

当使用python读取文件后打印结果意外多出了一个ufeff,如图: 那要如何解决呢?其实也很简单只要将encoding = 'utf-8' 改成encoding = 'UTF-8-sig'就搞定了,如图: 问题是解决了,但是这到底是什么原因呢? 答:在编写文本时保存时包含了BOM(Byte Order Mark,字节顺序标记,出现在文本文件头部,Un...

Docker——JVM 感知容器的 CPU 和 Memory 资源限制

前言 对于那些在Java应用程序中使用Docker的CPU和内存限制的人来说,可能会遇到一些挑战。特别是CPU限制,因为JVM在内部透明地设置GC线程和JIT编译器线程的数量。 这些可以通过命令行选项 -XX:ParallelGCThreads 和 -XX:CICompilerCount 显式设置。对于内存限制,也可以通过JVM命令行选项 -Xmx 显式设...

GB2312/GBK/GB18030/BIG5/UNICODE/UTF8编码

GB2312/GBK/GB18030/BIG5/UNICODE/UTF-8编码 - 3※5不甘平淡 - 51Testing软件测试网 51Testing软件测试网-中国软件测试人的精神家园 - Powered by X-SpaceGB2312/GBK/GB18030/BIG5/UNICODE/UTF-8编码经常会碰到一些关于编码的名词,特意收录了些资料以备...