个人成长博客

纸上得来终觉浅,绝知此事要躬行

0%

虚拟机类加载机制

虚拟机类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。在Java语言里面,类型的加载、连接和初始化过程都是在程序运行时期完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java可以动态扩展的特性就是依赖运行期动态加载和动态连接特性实现。

类加载的过程和时机

类的加载过程

类从被加载到虚拟机内存到卸载出内存,主要包含七个步骤如图:

其中,验证、准备、解析3个部分统称为连接,简单介绍下五个核心过程:

  1. 加载:通过ClassLoder加载class文件字节码,生成Class对象。获取二进制字节流,并将字节流所代表的静态存储结构转换为方法区的运行时数据,在内存中生成Class对象。
  2. 验证:检查加载的class的正确性和安全性,主要分为文件格式验证、元数据验证、字节码验证、符号引用验证。
  3. 准备:正式为类变量分配并设置类变量的初始值,这些变量所使用的内存都将在方法区中进行分配。
  4. 解析:解析是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
  5. 初始化:执行类变量赋值和静态代码块。

类的加载时机

加载、验证、准备、初始化和卸载这5个阶段顺序是确定的。类的解析在某些情况下可以在初始化阶段之后,这是为了支持Java语言运行时绑定。什么情况下开始加载,Java虚拟机规范中并没有进行强制约束,这点交由虚拟机具体实现自由把握。但对于初始化阶段,虚拟机严格规定了五种情况必须立即进行类的初始化(加载、验证、准备需要在此之前):

  1. 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,若类没有进行初始化,则需要先触发其初始化。生成这4条指令的场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(即包含main()方法的类),虚拟机会先初始化这个类。
  5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

类加载器

类加载器首先用于类的加载阶段,同时对于任意一个类,判断其唯一性要根据加载它的类加载器和这个类本身一同确立。判断两个类是否相等,只有在这两个类由同一个类加载器加载才有意义,只要加载它们的类加载器不同,这两个类必定不相等。

类加载器的种类

从Java虚拟机来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是用C++语言来实现的,是虚拟机的一部分;另一种就是所有其他的类加载器,这些都是Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。从Java开发人员来讲,则划分更细致,主要分为4种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):这个类加载器主要负责加载/lib或者被-Xbootclasspath参数指定的路径的类,并且是虚拟机识别的(按文件名识别,如Java核心库rt.jar,名字不符合的类库在lib目录中也不会被加载)
  2. 扩展类加载器(Extension ClassLoader):这个类加载器主要负责加载/lib/ext目录中的类库或者java.ext.dirs系统变量所指定路径中的所有类库。
  3. 应用程序类加载器(Application ClassLoader):它负责加载用户路径(ClassPath)上所指定的类库,如果没有自定义类加载器,一般就使用它作为默认的类加载器。
  4. 自定义类加载器:用户可以自定义类加载器。

首先先简单介绍下如何自定义ClassLoader:

1
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
40
41
42
43
import java.io.*;

public class MyClassLoader extends ClassLoader {
private String path;
private String classLoaderName;

public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}

//用于寻找类文件
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}

//用于加载类文件
private byte[] loadClassData(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}

首先继承ClassLoader类,然后覆写findClass方法,从指定目录中读取.class的二进制流,最后通过defineClass方法将二进制字节流定义为一个类并返回。

双亲委派机制

类加载器是互相配合进行加载的,有层次关系,这种层次关系称为类加载器的双亲委派模型,如图:

双亲委派模型中,除了顶层启动类之外,其余类加载器都有自己的父类加载器。这里的父子关系,是通过组合的方式来实现的。结合代码我们来分析双亲委派模型,实现双亲委派的代码都集中在ClassLoader的loadClass()方法之中,如下:

1
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
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) {
// 父类加载器无法加载时
// 调用本身的findClass方法进行类加载
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;
}
}

假设我们从自定义的ClassLoader开始进行加载,

  1. 首先自定义的ClassLoader会先查找是否已经加载该类,已经加载则返回,没有则从调用父类的loadClass()方法。
  2. 同理其父类AppClassLoader也是先查找自己是否已加载该类,有则返回,没有则有委派给它的父类ExtensionClassLoader。
  3. ExtensionClassLoader查找自己是否已加载该类,没有则委派给它的parent,因为它的parent为null,则去BootStrapClassLoader中去查找。
  4. BootStrapClassLoader查找并加载,若还没有,则通过ExtensionClassLoader的findClass方法去加载类。
  5. ExtensionClassLoader未能加载,则返回到AppClassLoader去尝试加载,若未能则继续到自定义的类加载器加载。
  6. 最后仍然不能加载则抛出ClassNotFoundException异常。

使用双亲委派机制来组织类加载器之间的关系,使得Java类随着类加载器一起具备了一种带有优先级的层级关系。这样对于高层类加载器加载的类库而言,则能够确保最终都是委派给高层的类加载器加载,这样也就能够确保各种类加载器环境中都是同一个类。

突破双亲委派机制

双亲模型固然有着优点,能够让整个系统保持了类的唯一性。但在有些场合,却不适合,也就是说,顶层的启动类加载器的代码无法访问到底层的类加载器。

如在 Java 平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常可以称为 Service Provider Interface,即 SPI。在 rt.jar 中的抽象类需要加载继承他们的在应用层的子类实现,rt.jar 无法中代码无法访问到应用类加载器,所以目前的双亲机制是无法实现的。因此 JDK 引用了一个不太优雅的设计,上下文类加载器。也就是讲类加载放在线程上下文变量中。通过 Thread.getContextClassLoader(), Thread.setContextClassLoader(ClassLoader) 这两个方法获取和设置 ClassLoader,这样,rt.jar 中的代码就可以获取到底层的类加载了。

双亲模式是虚拟机的默认行为,但并非必须这么做,通过重载 ClassLoader 可以修改该行为。事实上,很多框架和软件都修改了,比如 Tomcat,OSGI。具体实现则是通过重写 loadClass 方法,改变类的加载次序。比如先使用自定义类加载器加载,如果加载不到,则交给双亲加载。