JVM类的加载过程详解
# JVM类的加载过程
# 类的生命周期
# 类的加载过程
系统加载Class类的文件主要分为三步:加载、连接、初始化。链接过程包括:验证、准备、解析
加载:
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个类的元数据存储在方法区。(将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构)
- 将这个类的
java.lang.Class
对象存储在堆中。(在内存中生成一个代表这个类的java.lang.Class
对象,作为方法区这个类的各种数据的访问入口)
字节码来源:本地路径下编译生成的
.class
文件;jar包中的.class
文件;从远程网络以及动态代理实时编译的。连接-验证:为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。大致上完成四个阶段的检验:
- 文件格式验证:是否符合 class 文件的规范
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java 语言规范
- 字节码验证:确保程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
连接-准备:为类变量分配内存并设置类变量默认初始值,即零值。
不包含 final 修饰的 static,因为 final 在编译的时候就会分配好了默认值,准备阶段会显式初始化。
连接-解析:虚拟机将常量池内的符号引用转换为直接引用。
符号引用:以一组符号(字符串)来描述所引用的目标,这个字符串给出了一些能够唯一标识一个方法、一个变量、一个类的相关信息。
直接引用:可以理解为一个内存地址,或者一个偏移量,或者是一个能间接定位到目标的句柄。
例子:调用方法hello();方法的地址是123456,则hello是符号引用,12345是直接引用。
初始化:执行类初始化方法
<client>
的过程。对于初始化方法的调用,虚拟机会确保在多线程环境中的安全性(因为初始化方法带锁)。对于初始化阶段,虚拟机严格规范了有且只有6种情况,必须对类进行初始化:当遇到new、getstatic、putstatic、invokestatic这四条字节码指令时。
new 创建实例;getstatic 访问静态变量;pustatic 给静态变量赋值;invokestatic 调用静态方法。
使用反射时.
初始化一个类,其父类还未初始化,则先初始化父类。
当虚拟机启动时,用户需要定义一个要执行的主类(包含main方法的类),虚拟机会先初始化这个类。
动态用语言相关(略)
包含默认方法(被default关键字修饰的接口方法)的接口的实现类发生初始化,要先初始化接口。
# 类加载器
介绍
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
- 每个 Java 类都有一个引用指向加载它的 ClassLoader。
- 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
简单来说,类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。 字节码可以是 Java 源程序(.java文件)经过 javac 编译得来,也可以是通过工具动态生成或者通过网络下载得来。
其实除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。
# 三个重要的类加载器ClassLoader
- BootStrapClassLoader启动类加载器:由 c++ 实现,没有父级,主要用来加载 JDK 内部的核心类库(
%JAVA_HOME%/lib
目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被-Xbootclasspath
参数指定的路径下的所有类。 - ExtensionClassLoader扩展类加载器:Java 实现,主要负责
%JRE_HOME%/lib/ext
目录下的jar包,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 - AppClassLoader应用程序类加载器:面向用户的加载器,负责加载当前应用 classpath 下的所有 jar 包。
AppClassLoader 的父加载器为 ExtensionClassLoader
ExtensionClassLoader 的父加载器为 null,并不带代表没有父类加载器,而是 BootStrapClassLoader。
rt.jar:rt 代表“RunTime”,rt.jar是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.都在里面,比如java.util.、java.io.、java.nio.、java.lang.、java.sql.、java.math.*。
Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。
# 双亲委派模型
如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器(调用父类的loadClass方法),每一个层级的类加载器都是如此,因此所有类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,子类加载器才会尝试自己去加载。
为什么使用双亲委派模型:
对于任意一个类,都需要加载它的类加载器和这个类本身来一同确立其在 Java 虚拟机中的唯一性。
如果不是同一个类加载器加载,即使是相同 class 文件,也会出现判断不相同的情况,从而引发一些意想不到的情况,为了保证相同的 class 文件,在使用的时候是相同的对象,JVM 设计的时候,采用双亲委派的方式来加载类。
好处:
- 避免类的重复加载。Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,子类就没必要加在了。
- 保证Java核心API不被篡改。假设通过网络传递一个名为
java.lang.Integer
的类名,通过双亲委派模型传递到启动类加载器,而启动类加载器在核心API发现这个名字的类已被加载,就不会重新加载网络上传递过来的java.lang.Integer
,而是直接返回已加载过的Integer.class
。
# 自定义类加载器
除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader抽象类。
ClassLoader 类有两个关键的方法介绍:
protected Class loadClass(String name, boolean resolve)
:加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用resolveClass(Class<?> c)
方法解析该类。protected Class findClass(String name)
:根据类的二进制名称来查找类,默认实现是空方法。
如果不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
ClassLoader 类有两个关键的方法的源码:
方法 loadClass()
:如果想打破双亲委派模型则需要重写该方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,判断这个类是否已经被加载了
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
}
// 父类加载器加载完毕后 c 仍为 null,那就是父类加载器无发加载,由当前类加载器加载。
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
方法findClass()
:需要子类去实现,如果不想打破双亲委派模型,重写该方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
2
3
# 能否自定义一个java.lang.Object类?
类加载过程会遵循双亲委派原则,当一个类首次被加载时,会依次向上级类加载器委托,直到最顶层的 BootStrapClassLoader。java.lang.Object
属于系统类,会由 BootStrapClassLoader 优先加载,最终加载的还是系统原生的 java.lang.Object
类,因此会报找不到 main 方法的错误。
package java.lang;
public class Object {
public static void main(String[] args) {
System.out.println("test");
}
}
2
3
4
5
6
7
错误: 在类 java.lang.Object 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
2
3
正常情况下类加载过程会遵循双亲委派机制,依次向上级类加载器委托加载,上级都加载不了,才会自行加载。
如果想要绕过双亲委派机制,需要重写 ClassLoader 类中的 loadClass 方法,一般不推荐这么做。由于 final 方法 defineClass 的限制,正常情况下我们无法加载以 java.
开头的系统类。一般自定义类加载器只需要实现 ClassLoader 的 findClass 方法来加载自定义路径下的类,而不是覆写 loadClass 破坏双亲委派,避免带来系统安全隐患。