1.4.2 JVM加载字节码
文件加载分为两种情况。第一种情况是JVM直接加载字节码文件,按照文件格式进行解析、链接和初始化。关于字节码的加载过程已经有很多文章和书籍介绍过,这里不赘述。
类加载完成后,Java类的描述信息已经存储在JVM的内存空间中。JVM为了存储类描述信息,设计了Klass结构,而Java的实例化对象在JVM内部使用oop结构存储。从C++的角度来理解Klass和oop,可以把JVM中的Klass对象视为C++中的Class对象(Class是类,为了描述类信息,需要对象来存储,所以称为Klass对象),oop对象是C++中Class实例化的对象。
提示
在JDK 8之前,类的描述信息存放在Java堆中,称为永久代。从JDK 8开始,类的描述信息存放在JVM的本地堆中,这一空间称为元数据空间。详情参见JEP 122提案。这一提案的出发点是为了促进JRockit(JRockit是另一款Java虚拟机的实现,后被Oracle公司收购)和JVM的融合。JRockit没有永久代,所以JRockit客户不需要配置永久代,并且习惯于不配置永久代。所以把永久代从Java堆中移到了本地堆(即元数据空间)中,元数据空间的大小受限于物理内存的大小(当内存不足时可以直接从物理内存申请),而不是Java堆的大小,所以在一定程度上减少了元数据空间不足导致的内存溢出。
回顾C++编译器对多态的支持,使用虚函数表来记录不同类实现的虚函数。这个思路在JVM中同样适用。也就是说,JVM需要在维护的Klass结构中维护虚函数表。JVM中描述Java类对象的Klass(更准确的类型是InstanceKlass)结构如图1-13所示。
图1-13 JVM中Klass结构示意图
在图1-13中,Klass中有一个vtable,等价于C++编译器中的vtbl。另外,Klass中的itable的作用类似于vtable,主要原因是Java语言只支持单继承和多接口,itable对应的就是接口的实现。Klass中还有一个oop map,这个变量与垃圾回收紧密相关,该信息用于支持精确垃圾回收,更多信息可参考第2章。除此以外,Klass还有几个重要的成员,图1-13中都已经展开介绍。
JVM文件加载的第二种情况是加载通过jaotc(JDK 9开始支持该功能)编译产生的可执行文件,加载过程实际类似于字节码,只不过文件格式不同。另外的不同点还有可执行文件中包含了一些额外的信息,这些信息用于垃圾回收、动态链接等。由于该特性已经从JDK 17中移除,因此本书不再进一步讨论。
另外,Java语言有一个特别的设计,即所有的引用类型都继承于Object类(此类是Java类库定义的基础类),Object类有5个方法是虚函数,所以任意的引用类型也都会包含这5个虚函数,并且对于引用类型定义的函数(默认函数都是虚函数,除非显式地使用final、static等修饰符限定)会添加在自己的虚函数中。Object默认的虚函数如图1-14所示。
图1-14 Object默认定义的虚函数
提示
读者可以通过诸如HSDB等工具查看Java代码中定义的虚函数。关于HSDB工具的使用可以参考其他文献。
另外,Object类中还有wait/notify等方法,它们使用final等修饰符限定后,不属于虚方法,直接编译成类似于C++语言的静态方法。JVM规范中还设计了不同的字节码用于执行不同类型的函数调用,例如使用字节码invokevirtual执行虚函数,使用字节码invokestatic执行静态函数。