8.1.12直接引用
•常量池解析的最终目标是把符号引用替换为直接引用。符号引用的格式在第6章中详细定义了,但是直接引用应该是什么格式呢?你可能认为,直接引用的格式也是由不同的java虚拟机实现的设计者决定的。然而,在大多数实现中,总会有一些通用的特征。
指向类型、类变量和类方法的直接引用可能是指向方法区的本地指针。类型的直接引用可 能简单地指向保存类型数据的方法区中的与实现相关的数据结构。类变量的直接引用可以指向方法区中保存的类变量的值。类方法的直接引用可以指向方法区中的一段数据结构方法区中包含调用方法的必要数据)。比如,类方法的数据结构可能包含方法是否为本地方法的标志信息。 如果方法是本地的,数据结构可能包含一个指向动态连接的本地方法实现的函数指针。如果方法不是本地的,数据结构可能包含方法的字节码、max_stack、max_local等信息。如果有—个该方法的即时编译版本,数据结构可能包含指向即时编译的本地代码的指针。
指向实例变量和实例方法的直接引用都是偏移量。实例变量的直接引用可能是从对象的映像开始算起到这个实例变量位置的偏移量。实例方法的直接引用可能是到方法表的偏移量。
使用偏移量来表示实例变量和实例方法的直接引用,取决于类的对象映像中字段的顺序和 类方法表中方法的顺序的预先决定。尽管不同实现的设计者可以选择在对象映像中存放实例变量的方式,以及在方法表中存放方法的方式,但几乎可以肯定的是,他们对所有的类型都使用 同样的方式。所以,在任何实现中,对象中字段的顺序和方法表中方法的顺序是被定义好的, 也是可以预测的。
例如,考虑下面的由三个类和一个接口组成的层次关系。
// On CD-ROM in file linking/ex4/Friendly.java
interface Friendly { void sayHello();
void sayGoodbye();
}// On CD-ROM in file linking/ex4/Dog.java
class Dog { // How many times this dog wags its tail when
// saying hello.
private int wagCount = ((int) (Math.random() * 5.0)) + 1; void sayHello() {
System.out.print("Wag");
for (int i = 0; i < wagCount; ++i) {
System.out.print(", wag");
}
System.out.println(".");
} public String toString() {
return "Woof!";
}
}// On CD-ROM in file linking/ex4/CockerSpaniel.java
class CockerSpaniel extends Dog implements Friendly { // How many times this Cocker Spaniel woofs when saying hello.
private int woofCount = ((int) (Math.random() * 4.0)) + 1; // How many times this Cocker Spaniel wimpers when saying
// goodbye.
private int wimperCount = ((int) (Math.random() * 3.0)) + 1; public void sayHello() {
// Wag that tail a few times.
super.sayHello(); System.out.print("Woof");
for (int i = 0; i < woofCount; ++i) {
System.out.print(", woof");
}
System.out.println("!");
} public void sayGoodbye() {
System.out.print("Wimper");
for (int i = 0; i < wimperCount; ++i) {
System.out.print(", wimper");
}
System.out.println(".");
}
}// On CD-ROM in file linking/ex4/Cat.java
class Cat implements Friendly { public void eat() {
System.out.println("Chomp, chomp, chomp.");
} public void sayHello() {
System.out.println("Rub, rub, rub.");
} public void sayGoodbye() {
System.out.println("Scamper.");
} protected void finalize() {
System.out.println("Meow!");
}
}
假设装载这些类型的Java虚拟机采用的组织对象的方式是,实例变量在子类中声明之前,就 把在超类中声明的该实例变量放到了对象映像中;并且每一个类的实例变量出现的顺序和它们在class文件中出现的顺序是一致的。假设类Object没有实例变量,Dog、CockerSpaniel和Cat的对象映像如图8-1所示:
在图8-1中,CockerSpaniel的对象映像很好地说明了这个特定的虚拟机排列对象的方式:来 自超类Dog的实例变量出现在来自子类CockerSpaniel的实例变量之前。CockerSpaniel的实例变量按照它们声明的顺序出现:先是woofCounl,然后是wimperCount。
注意实例变量wagCount在Dog和CockerSpaniel中都作为偏移量1出现。在这个Java虚拟机实现中,指向类Dog的wagCount字段的符号引用会被解析成为一个偏移量为1的直接引用。不管实 际的对象是Dog、CockerSpaniel,或任何Dog的子类,实例变量wagCount总是在对象映像中作为偏移量1出现。
在方法表中也呈现出同样的情形。方法表中的一个人口以某种方式关联到方法区中的—段数据结构(方法区包含让虚拟机调用此方法的足够信息),假设在我们现在描述的java虚拟机实现中,方法表是关于指向方法区的指针的数组。方法表人口指向的数据结构和我们前面提到的类方法的数据结构类似。假设这种特定的Java虚拟机实现装载方法表的方法是,来自超类的方法出现 在来自子类的方法之前;并且每个类排列指针的顺序和方法在C|ass文件中出现的顺序相同。这种 排列顺序的例外情况是,被子类的方法覆盖的方法出现在超类中该方法第—次出现的地方。
这个虚拟机组织Dog类方法表的情况如图8-2所示:在该图中,指向在类Object中定义的方法的方法表人口,在图中显示为深灰色;指向在Dog中定义的方法的人口显示为浅灰色。
注意在这个方法表中只有非私有的实例方法才会出现.用invokestatic指令调用的类方法不 在这里出现,因为它们是静态绑定的,不需要在方法表的间接指向。私有的方法和实例的初始化方法不需要在这里出现,因为它们是被invokespeeial指令调用的,所以也是静态绑定的。只有被invokevirtual或者invokeinterface调用的方法才需要出现在的这个方法表中。参见第19章讨论的调用指令。
通过源代码,可以看到Dog覆盖了Object类中定义的toString ()方法。在Dog的方法表中. toString ()方法只出现了-次,在Object的方法表中出现的同样的位置出现(偏移量7 )。在Dog的方法表中,这个指针位于偏移量7,并且指向Dog的toString ()实现的数据。在这个Java虚拟机实现中,指向toString()方法数据的指针会在每个类的方法表中都处于偏移量7。(实际上, 可以编写一个定制版本的java.lang.Object,并且用一个自定义的类装载器来装载:用这种方法, 可以创建一个命名空间,使用同样的虚拟机,在这个命名空间中指向toString ()方法的指针就 可以不处于方法表的偏移量7的位置。)
位于Object中声明的方法下方的第一个方法,是Dog中声明的方法,这些方法没有重载Object 的方法,只有一个sayHello ()方法,位于方法表偏移量11。所有Dog的子类都会继承或者覆盖 这个sayHello ()方法的实现,并且所有Dog的子类的sayHello ()会一直出现在偏移量11。
图8-3显示了CockerSpaniel的方法表。注意,因为CoclcerSpaniel声明了sayHello()和sayGoodbye(),这些方法的指针指向这些方法的CockerSpaniel实现的数据〔因为CockerSpaniel 继承了Dog的toString ()实现,该方法的指针(仍然在偏移量7 )指向Dog的该方法的实现的数 据。CockcrSpaniel继承了Object的所有其他方法,所以这些方法的指针直接指向Object的类型数据。注意sayHello()仍然位于偏移量11,和它在Dog的方法表中的偏移量一致。
当虚拟机解析一个符号引用(CONSTANT_Methodref_info人口)到任何类的toString()方法的时候,它指向方法表偏移量7。当虚拟机解析指向Dog或者任何子类的sayHello ()方法的 符号引用的时候,直接引用是方法表偏移量11。当虚拟机解析指向CockerSpaniel或者任何子类 的sayGoodbye ()方法的符号引用的时候,直接引用就是方法表偏移量12。
一旦一个指向实例力法的符号引用被解析为—个方法表偏移量后,虚拟机仍然需要实际调用此方法。要调用一个实例方法,虚拟机在对象中搜寻对象的类的方法表。在5章中讲过,给定一个指向对象的引用,每一个虚拟机实现必须有办法找到这个对象的类的类型数据。除此之 外,给定一个指向对象的引用,方法表(对象的类的类型数据的一部分)—般需要非常快地访问。 (图5-7显示了一种可能的情形)-旦虚拟机有了对象的类的方法表,它用偏移量来找到正确的需要调用的方法。
当虚拟机有一个指向类类型的引用(CONSTANT_Methodref_info人口 )的时候,它总是可 以依靠方法表偏移量。如果在Dog类中sayHello ()方法出现在偏移量7,那么在Dog的所冇子类 中它都出现在偏移量7。不过当引用是指向接口类型(CONSTANT_interfaceMethodref_info人口)的时候,这就不成立了。当通过接口引用来访问实例方法的时候,直接引用不能保证得到方法表偏移量。考虑一下图8-4中的Cat类的方法表。
注意Cat和CockerSpsmiel都实现了Friendly接口,一个类型为Friendly的变量可能保存的是指向Cat对象的引用,也可能是指向CockerSpaniel对象的引用。用这个引用,你的程序可以调用Cat或者CockerSpaniel或者任何其他实现了Friendly接口的对象的sayHello ()和sayGoodbye () 方法。Example4程序演示了这一点:
// On CD-ROM in file linking/ex4/Example4.java
class Example4 { public static void main(String[] args) {
Dog dog = new CockerSpaniel();
dog.sayHello();
Friendly fr = (Friendly) dog;
// Invoke sayGoodbye() on a CockerSpaniel object through a
// reference of type Friendly.
fr.sayGoodbye(); fr = new Cat();
// Invoke sayGoodbye() on a Cat object through a reference
// of type Friendly.
fr.sayGoodbye();
}
}
在Example4中,本地变量fr调用了CockerSpaniel对象和Cat对象的sayGoodbye ()方法,调 用二者的该方法使用的是常量池中的同一个CONSTANT_ InterfaceMethodref_info人口。但是当 虚拟机解析指向sayHello ()的符号引用的时候,它不能只保存一个方法表偏移量,然后指望将 来对常量池人口操作时依赖这个偏移量。
麻烦之处在于,实现Friendly接口的类并不能保证都是从同一个超类继承的,这个超类也同 样实现Friendly接口。这样,Friendly中声明的方法并不能保证处于方法表的同一位置。比如, 如果比较一下CockerSpaniel和Cat的方法表的话,会发现在CockerSpaniel中,sayHello ()位于 偏移量11;但是在Cat中,sayHello ()出现在偏移量12。同样,CockerSpaniel的sayGoodbye () 方法的指针位于偏移量12,而Cat的sayGoodbye ()方法的指针位于偏移量13。
因此,不管何时Java虚拟机从接口引用调用一个方法,它必须搜索对象的类的方法表来找到 一个合适的方法。这种调用接口引用的实例方法的途径会比在类引用上调用实例方法慢很多。 当然,关于如何搜索类的方法表,虚拟机实现可以灵活一些。例如,实现可以保存最后找到方 法时的索引,然后在下次搜索时首先查找该位置。或者某些实现可能在准备的时候就建立一些 数据结构,这有助于在给定一个接口引用的时候搜索方法表。不管怎样,给定接口引用时调用 方法总是比给定类引用时调用方法慢得多。