浅析类加载过程与类加载器

前言

当我们完成了一个Java类的逻辑编写,如果想要进行逻辑的执行,就需要把他放在JVM虚拟机中进行运行.整个放入加载的过程就是由类加载器来完成的.

类加载过程and类加载器的作用

我们使用Java语言写出的每一个.java的文件中都存储着需要执行的程序逻辑,但这样的文件JVM是不会识别的,所以想要运行一个程序,首先要由javac编译器把.java的文件编译成为.class的文件(这一步是在JVM外部完成的),在这个.class文件中保存了Java代码经过转换后的虚拟机指令,在需要使用某个类的时候,虚拟机会加载他的.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程就是类加载的过程,完成这个过程的代码片段就叫做类加载器.

类加载全过程

类加载全过程大体来说分为三部分:
1.加载(Loading)
2.链接(Linking),链接又分为验证,准备和解析三个部分
3.初始化(Initialization)

具体加载过程分为五步,如下图所示

每一步的作用:

  1. 加载: 类加载的一个过程,会根据一个类的完全限定查找这个类对应的字节码文件,并利用字节码文件创建一个Class对象
  2. 链接:
  • 验证: 主要用来确保当前准备加入虚拟机中的字节码文件的可靠性,确保Class文件中的字节流中包含信息符合当前虚拟机的要求不会对虚拟机造成什么危害
  • 准备: 为类变量,即使用static修饰的全局变量进行内存的分配和数据的初始化,注意此处的初始化不包含final static 修饰的变量,因为被final修饰的静态变量在编译的时候就已经在JVM运行时数据区的方法区中为他开辟好了空间;当然也不会为实例变量分配空间,他们是在类对象初始化的时候在堆上进行空间的分配
  • 解析: 主要将常量池中的符号引用替换为直接引用的过程
  1. 初始化: 这是类加载的最后阶段,如果该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了的static的静态成员此时进行赋值,成员变量也在这时候进行初始化)

以上就是类加载的具体过程.而类加载器的任务就是根据一个类的全限定名找到他所对应的class文件,然后读取此文件的二进制流到JVM中,转换为一个与目标类对应的java.lang.Class对象实例.

类加载器

在虚拟机中共提供了三种类加载器,启动类加载器(BootStrap),扩展类加载器(ExtClassLoader),应用程序类加载器(AppClassLoader),下面将分别进行介绍

BootStrap(启动类加载器)

启动类加载器主要用来加载JVM自身需要的一些类,这个类由c++编写,是虚拟机自身的一部分,他负责将<JAVA_HOME>/lib 路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意,虚拟机是按照文件名识别加载jar包的,如dt,jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录底下也是没有作用的(Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类). 启动类加载器不能被Java程序直接调用

ExtClassLoader(扩展类加载器)

扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,是Launcher的静态内部类,由java语言实现.负责加载<Java_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量指定的路
径中的类库。开发者可以直接使用扩展类加载器

AppClassLoader(应用程序类加载器)

也称应用程序加载器是指Sun公司实现的sun.misc.Launcher$AppClassLoader.他负责加载用户路径上(classPath)指定的类库,我们开发人员一般就是直接使用这个类加载器的,一般情况下,该类加载器也是程序中默认的类加载器如果没有自定义类加载器的话


在程序开发中,一般都是由这三个类加载器相互协调配合进行加载的,在必要时,有时会加入自定义类加载器,需要注意的是,JVM只有在需要某一个类时才会加载他的字节码文件,生成Class对象,不需要时就不会先进行加载,而且加载class文件时,Java虚拟机采用的是双亲委派模式,接下来将介绍一下双亲委派模式

双亲委派模式

在了解双亲委派模式之前,需要先说明一下这些类加载器之间的关系.

如图所示的各种类加载器之间的这种层次关系就成为类加载器的双亲委派模型.
双亲委派模型要求除了顶层的类加载器之外,下层的类加载器都应该有自己的父类加载器 ,需要注意的是,这里所说的父类关系并不是通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码

双亲委派模式工作原理

双亲委派模型的工作流程是: 如果一个类加载器收到了类加载请求,他不会立马自己去进行类加载,而是把类加载的请求委托给父类加载器去完成,每一层的加载器都是如此.因此,一个类加载请求总会传送到顶层的BootStrap加载器中,只有当父类加载器无法满足加载请求并反馈给自己的子类加载器时(在自己的搜索范围内没有此类),子加载器才会尝试自己去加载. 即儿子都很懒,有活了都想交给自己的父亲去做,知道父亲说这个活我没有办法帮你做时,儿子才会尝试自己去做
双亲委派模型不是强制的,可以破坏双亲委派模型去进行类加载,例如OSGI技术

双亲委派模型的优势

1.避免了类的重复加载 : 扩展类加载器和程序应用类加载器都是可以被直接调用的,如果没有双亲委派模型,那么完全可以调用两个类加载器对同一个类进行两次加载;而双亲委派模型中Java类随着他的类加载器一起具备了一种带有优先级的层次关系.通过这种层次关系就可以避免了类的重复加载,只要父亲加载过,儿子就不需要在此加载
2.提高了程序的安全性 : 例如加载java.lang.Object类,它存在于rt.jar中,无论哪一个类加载器要加载这个类,最终都是委托给最顶层的BootStrap类加载器进行加载,因此Object类在程序的各个类加载器环境中都是同一个类.如果没有双亲委派模型,而是由各个类加载器自行加载的话,那用户自己编写一个java.lang.Object的同名类并放在ClassPath中加载,那么系统中就会出现多个不同的Object类,造成程序的混乱与不安全;而在双亲委派机制下,用户自己写的这个Object类会传到顶层的BootStrap中,他会在/lib路径下寻找,并没有发现有我们自己定义的这个类,所以即使类名完全一样也不会担心被误加载进来,大大提高了程序的安全性

ClassLoader类中loadClass()方法与双亲委派模型

在Java中具体的类加载器是ClassLoader类提供的,他是一个抽象类,里面包含了许多的方法,Java中许多实现好的类加载器都是直接或间接的继承自此抽象类,下面以图的形式说明一下各个类加载器直接的继承关系

从图中可以看出来最顶层的类加载器是ClassLoader,他是一个抽象类,其他的类加载器全部都继承自ClassLoader(不包括启动类加载器).在ClassLoader中有许多的方法,在这里说一下loadClass方法,他是ClassLoader抽象类自己实现的方法,这个方法中的逻辑就是双亲委派模型实现的
loadClass(String name, boolean resolve)源码如下:

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,在本地缓存中查找该class对象,如果有就不需要再次加载了
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;
}
}

1.首先这个方法有两个参数,第一个参数就是我们要输入的类的全名称,也就是我们需要JVM帮助我们加载的那个类;第二个参数,resolve参数代表是否生成class对象的同时进行解析相关操作.
2.就像上面代码展示的一样,当前类加载器接收到了加载的指令,他会首先在本地的缓存中去查找是否有当前类的对象,如果有当前类的对象,就不用去进行加载了;如果没有就会将加载请求委托给自己的父类加载器,如果该加载器没有父类加载器的话,就会直接交给顶级类加载器—-启动类加载器,最后如果还是没能加载类,就调用findClass()方法去找自定义类加载器.
这个方法就是双亲委派模型的一种实现形式 ~~~
最后再总结一下类加载器之间的关系

  • 启动类加载器: 由c++实现,没有父类
  • 扩展类加载器: 由Java语言实现,父类加载器为null
  • 应用程序类加载器: 由Java语言实现,父类为ExtClassLoader
  • 自定义类加载器: 父类加载器为AppClassLoader