类可用机制
一个类需要经过漫长的旅程才能被虚拟机其他组件,如解释器、编译器、GC等在运行时使用,下面将详细介绍类的一个完整生命周期,即加载、链接、初始化三部曲。
类的加载
类加载过程先于虚拟机的绝大部分组件的加载过程,具体会在第4章讲解。虚拟机初始化完成后做的第一件事情就是加载用户指定的主类。类加载也是类可用机制的第一步,它负责定位并解析位于磁盘(通常)的字节码文件,生成一个包含残缺数据的用于在JVM内部表示类的数据结构,然后将该结构传递给下一步链接做后续工作。
字节码
Java源码通过javac(Java编译器)编译生成字节码,然后将字节码送入虚拟机运行。字节码是Java源码的一种紧凑的二进制表示,它相对于Java源码来说比较低级,但是更符合机器模型,更容易被机器“理解”。以代码清单2-1的Java代码为例:
代码清单2-1 加法示例源码
public class Foo{
public static void main(String[] args){
int a· = 3;
int b = a+2;
System.out.println(b);
}
}
使用javac编译Foo.java得到二进制字节码文件Foo.class,但二进制的Foo.class难以被人类理解,为了直观地查看编译后的字节码,可以使用JDK中的javap -verbose Foo.class输出人类可读的字节码,部分输出如代码清单2-2所示:
代码清单2-2 加法示例字节码
0: iconst_3
1: istore_1
2: iload_1
3: iconst_2
4: iadd
5: istore_2
6: getstatic#2 // Field Ljava/io/PrintStream;
9: iload_2
10: invokevirtual#3 // Method java/io/PrintStream.println:(I)V
13: return
字节码中的#2表示常量池索引2的位置,后面注释说明了该位置表示被调用的方法,这样后面的字节码可以使用字节索引而不需要表示函数的字符串,在减少冗余的同时节省了空间。可以看到两个变量相加被编译成了栈操作:iconst_3压入3到操作栈,istore_1读取栈顶的3到变量a,然后iload_1读取a并入栈,iconst_2压入2,iadd弹出a和2并将结果入栈,istore_2将刚刚计算得到的结果即栈顶弹出放入b,最后输出。
也正是由于字节码对于源码的描述是栈的形式,所以Java虚拟机属于栈式机器(Stack Machine)。与之相对的是寄存器机器(RegisterMachine),如代码清单2-3所示的Lua字节码,它对上面加法的描述截然不同:
代码清单2-3 luac -l -p生成的加法字节码
-- lua源码
a = 3
b = a + 2
io.write(b)
-- lua字节码
0+ params, 2 slots, 1 upvalue, 0 locals, 6 constants, 0 functions
1 [1] SETTABUP 0 -1 -2 ; _ENV "a" 3
2 [2] GETTABUP 0 0 -1 ; _ENV "a"
3 [2] ADD 0 0 -4 ; - 2
4 [2] SETTABUP 0 -3 0 ; _ENV "b"
5 [3] GETTABUP 0 0 -5 ; _ENV "io"
6 [3] GETTABLE 0 0 -6 ; "write"
7 [3] GETTABUP 1 0 -3 ; _ENV "b"
8 [3] CALL 0 2 1
9 [3] RETURN 0 1
寄存器机器的加法是直接使用add 0 0 -4指令完成的,它的操作数和指令组成一个整体,而栈式机器的iadd没有操作数,它隐式地假设了一个操作数栈,用于存放iadd需要的数据,这是两者的主要区别。寄存器机器和栈式机器很大程度上是指虚拟机指令集(InstructionSet Architecture,ISA)的特点,与虚拟机本身如何实现并无关系。当然,这并不是说寄存器机器就是用寄存器执行指令的虚拟机,事实上,很多寄存器机器都是用数组模拟寄存器执行读写指令的。寄存器机器的指令集更紧凑,性能也可能更好;栈式机器的指令集易于编译器生成,两者各有千秋,并无绝对优势的一方。
类加载器
在了解了Java字节码的基本概念后,就可以步入类可用机制的世界了。前面提过,javac编译器编译得到字节码,然后将字节码送入虚拟机执行。实际上送入虚拟机的字节码并不能立即执行,它与视频文件、音频文件一样只是一串二进制序列,需要虚拟机加载并解析后才能执行,这个过程位于ClassLoader::load_class()。
ClassLoader是虚拟机内部使用的类加载器,即Bootstrap类加载器。
除了Bootstrap类加载器外,HotSpot VM还有Platform类加载器和Application类加载器,它们三个依次构成父子关系(不是代码意义上由承构造出来的父子关系,而是逻辑上的父子关系)。虚拟机使用双亲委派机制加载类。当需要加载类时,首先使用Application类加载器加载,由Application类加载器将这个任务委派给Platform类加载器,而Platform类加载器又将任务委派给Bootstrap类加载器,如果Bootstrap类加载器加载完成,那么加载任务就此终止。如果没有加载完成,它会将任务返还给Platform类加载器等待加载,如果Platform类加载器也无法加载则又会将任务返还给Application类加载器加载。每个类加载器对应一些类的搜索路径,如果所有类加载器都无法完成类的加载,则抛出ClassNotFoundException。双亲委派加载模型避免了类被重复加载,而且保证了诸如java.lang.Object、java.lang.Thread等核心类只能被Bootstrap类加载器加载。
在HotSpot VM中用ClassLoader表示类加载器,可以使用ClassLoader::load_class()加载磁盘上的字节码文件,但是类加载器的相关数据却是存放在ClassLoaderData,简称CLD。源码中很多CLD字样指的就是类加载器的数据。每个类加载器都有一个对应的CLD结构,这是一个重要的数据结构,如图2-1所示。
CLD存放了所有被该ClassLoader加载的类、当前类加载器的Java对象表示、管理内存的metaspace等。另外CLD还指示了当前类加载器是否存活、是否需要卸载等。除此之外,CLD还有一个next字段指向下一个CLD,所有CLD连接起来构成一幅CLD图,即ClassLoaderDataGraph。
通过调用
ClassLoaderDataGraph::classes_do可以在垃圾回收过程中很容易地遍历该结构找到所有类加载器加载的所有类。
文件解析
ClassLoader::load_class()负责定位磁盘上字节码文件的位置,读取该文件的工作由类文件解析器ClassFileParser完成,如代码清单2-4所示:
代码清单2-4 类文件解析器
void ClassFileParser::parse_stream(...) {
// 开始解析
stream->guarantee_more(8, CHECK);
// 读取字节码文件开头的魔数,即0xcafebabe
const u4 magic = stream->get_u4_fast();
guarantee_property(magic == JAVA_CLASSFILE_MAGIC,...);
// 读取major/minor版本号
_minor_version = stream->get_u2_fast();
_major_version = stream->get_u2_fast();
// 读取常量池
...
// 读取this_class和super_class
_this_class_index = stream->get_u2_fast();
Symbol* class_name_in_cp = cp->klass_name_at(_this_class_index);_class_name = class_name_in_cp;
...
}
Java所有的类最终都继承自Object类,每个类的常量池都会包含诸如“[java/lang/Object;”的字符串。为了节省内存,HotSpot VM用Symbol唯一表示常量池中的字符串,所有Symbol统一存放到SymbolTable中。
SymbolTable是一个并发哈希表,虚拟机会根据该表中Symbol的哈希值判断是返回已有的Symbol还是创建新的Symbol。
SymbolTable有个特别的