JVM 类加载机制
# 一、概念
类加载器子系统负责加载 class 文件,class 文件在文件开头有特定的文件标识,将 class 文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且类加载器只负责 class 文件的加载,至于其是否可以运行则由执行引擎决定。
其位置如下图所示:
# 二、类加载过程
从上图就可以看出来,类加载分为三个大步骤、五个小步骤:
- 加载(Loading)
- 链接(Linking)
- 验证(Verify)
- 准备(Prepare)
- 解析(Resolve)
- 初始化(Initialization)
# 2.1 加载
加载就是通过一个类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,然后在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口。
class 文件的加载方式:
- 从本地系统直接加载;
- 通过网络获取,如:Web Applet;
- 从 zip 压缩包读取,程维以后 jar、war 格式的基础;
- 运行时计算生成,如:动态代理;
- 由其它文件生成,如:jsp;
- 从专门的数据库中提取 class 文件;
- 从加密文件获取,如:防 class 文件被反编译的保护措施。
# 2.2 验证
验证的目的在于确保 class 文件的字节流中包含的信息符合当前虚拟机要求,确保被加载类的正确性,不会危害到虚拟机自身的安全。
验证包括文件格式验证、元数据验证、字节码验证、符号引用验证等。
# 2.3 准备
准备的目的是为类变量分配内存并且设置该类变量的默认初始值。
但需要注意的是:
- 准备阶段不包含 final 修饰的 static 部分,因为 final 在编译阶段已经确定,准备阶段只是显式初始化。
- 准备阶段不会为实例变量分配初始化。因为类变量会分配在方法区中,而实例变量会随着对象一起分配到 Java 堆中。
类变量和实例变量如何理解?
# 2.4 解析
解析就是将常量池的符号引用转换为直接引用的过程。符号引用就是一组符号来描述所引用的目标;直接引用就是直接指向目标的指针、相对偏移量或一个简介定位到目标的句柄。
解析的对象包括接口、字段、类方法、接口方法、方法类型等。
# 2.5 初始化
初始化就是执行类构造器的阶段,构造方法中的指令按语句在源文件中出现的顺序执行。若该类有父类,JVM 会保证在执行子类构造方法之前父类的构造方法已经执行完毕。
# 三、类加载器
# 3.1 类加载器分类
JVM 自带的类加载器有启动类加载器(Bootstrap Class Loader)、扩展类加载器(Extension Class Loader)和应用类加载器(Application Class Loader),除了 JVM 自带的类加载器还可以通过继承抽象类 java.lang.ClassLoader
自定义类加载器。
# 3.1.1 JVM 自带的类加载器
- 启动类加载器(Bootstrap Class Loader):该加载器使用 C/C++ 语言实现,加载存放在
<JAVA_HOME>/lib
目录,或者被-Xbootclasspath
参数所指定的路径中存放的,而且是 Java 虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机的内存中。 - 扩展类加载器(Extension Class Loader):该加载器使用 Java 语言实现(sun.misc.Launcher$ExtClassLoader),父类加载器为启动类加载器。从 java.ext.dirs 系统属性指定的目录或 JAVA_HOME/jre/lib/ext 目录下的类。
- 应用类加载器(Application Class Loader):该加载器使用 Java 语言实现(sun.misc.Launcher$AppClassLoader),父类加载器为扩展类加载器,负责加载 classpath 或系统属性 java.class.path 指定路径下的类。
# 3.1.2 自定义类加载器
继承 java.lang.ClassLoader
抽象类。在 JDK1.2 之前,自定义类加载器需要集成 ClassLoader 类并重写 loadClass()方法。但是在 JDK1.2 之后不建议覆盖 loadClass()方法,而是建议将自定义类的加载逻辑卸载 findClass()方法中。在自定义类加载器时,如果没有太复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免自己编写 findClass()方法及其获取字节流的方式。
# 3.2 双亲委派机制
# 3.2.1 概念
当一个类加载器收到了类加载请求,首先不会尝试自己去加载此类,而是将其请求委派给父类去完成,父类也会将其尝试委派给自己的父类去完成,因此所有的加载请求都最终会传送到启动类加载器中。只有当父类加载器反馈自己无法完成这个请求的时候,也就是在自己的加载路径下没有找到所需要的 Class,子类加载器才会尝试自己去加载。
# 3.2.2 代码实现
// java.lang.ClassLoader()#loadClass()
// 先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的 loadClass()方法,
// 若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,
// 抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 3.2.3 优点
- 避免类重复加载;
- 保护程序安全、防止核心 API 被篡改,即沙箱安全机制。
# 3.2.4 缺点
双亲委派机制的核心思想是越是基础的类就由越上层的类加载器加载,这样的话用户的代码调用基础的 API 是没有问题的,但是础代码就不能调用用户代码了。或者说父加载器所加载的代码不能调用子加载器加载的代码。
# 四、类文件结构
Class 文件是一组以 8 个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用 8 个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个 8 个字节进行存储。
# 4.1 魔数
每个 Class 文件的前四个字节被称为魔数,它的作用仅仅是迎来标识该文件是可以被 JVM 解释的 Class 文件。作用和文件后缀名类似,但是后缀容易被篡改。
Class 文件的魔数是 CAFEBABE。
# 4.2 Class 文件版本号
第五个和第六个字节是次版本号,第七个和第八个字节是主版本号。
# 4.3 常量池
紧接着主、次版本号之后的是常量池入口。由于常量池中的数量不是固定的,所以在常量池入口需要一个 u2 类型的数据来表示常量池容量计数值。常量池中存储字面量(Literal)和符号引用(Symbolic References)。
# 4.4 访问标志
紧接着常量池之后的两个字节表示访问标志,具体包括:此 Class 是类还是接口、是否定义为 public 类型、是否定义为 abstract、如果是类的话是否被声明为 final 等。
# 4.5 类索引、父类索引、接口索引集合
访问标志之后的是类索引、父类索引进而接口索引集合。Class 文件中由这三项数据来确定该类型的继承关系:类索引用于确定这个类的全限定名;父类索引用于确定这个类的父类的全限定名;接口索引集合用来描述这个类实现了哪些接口,被实现的接口将按照 implements 关键字后的接口顺序从左到右排列。
# 4.6 字段表集合
字段表用于描述接口或者类中声明的变量,包含类级变量以及实例级变量,但不包括方法内部的局部变量。字段被一系列布尔值型的标志位进行修饰,具体包括:作用域(public、private、protected)、是实例变量还是类变量(static)、可变性(final)、并发可见性(volatile)、是否可被序列化(transient)、字段数据类型(基本类型、对象、数组)。
# 4.7 方法表集合
方法表和字段表的概念相似,用于描述接口或类中声明的方法。方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)。这里并没有包含方法中定义的代码块,代码块在属性表中的 Code 属性中。
# 4.8 属性表集合
JVM 对属性表的限制相比其它部分要宽松很多,对于每一个属性,它的名称都要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说明属性值所占用的位数即可。
- Code 属性:Java 程序方法体里面的代码经过编译之后的字节码指令存储在 Code 属性中,但是接口或抽象类中没有方法体的方法不存在 Code 属性。
- Exceptions 属性:列举出方法中可能抛出的异常。
- LineNumberTable 属性:用于描述 Java 源码行号与字节码行号之间的对应关系。
- LocalVariableTable 与 LocalVariableTypeTable 属性:用于描述栈帧中局部变量表的变量与 Java 源码中定义的变量之间的关系。LocalVariableTypeTable 与 LocalVariableTable 非常相似,仅仅是把记录的字段描述符的 descriptor_index 替换成了字段的特征签名。
- SourceFile 与 SourceDebugExtension 属性:SourceFile 用以记录生成这个 Class 文件的源码文件名,SourceDebugExtension 属性用于存储额外的代码调试信息。
- ConstantValue 属性:用于通知 JVM 自动为静态变量赋值。
- InnerClasses 属性:用于记录内部类与宿主类之间的关联。
- Deprecated 与 Synthetic 属性:这两个属于标志类型的布尔属性,Deprecated 属性用于表示某个类、字段或方法已经不推荐使用,Synthetic 属性用于表示字段或方法并不是 Java 源码产生的,而是由编译器自行添加的。
- StackMapTable 属性:这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
- Signature 属性:Signature 属性在 JDK 5 增加到 Class 文件规范之中,它是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。在 JDK 5 里面大幅增强了 Java 语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则 Signature 属性会为它记录泛型签名信息。
- BootstrapMethods 属性:用于保存 invokedynamic 指令引用的引导方法限定符。
- MethodParameters 属性:用于记录方法的各个形参名称和信息。
← JVM 架构 JVM 运行时数据区→