类加载机制

类加载过程

类的加载过程包括加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备和解析合称为连接,加载过程如下图所示:
paste image
加载过程除解析以外其他步骤顺序都是确定的,为了支持java的动态绑定,解析阶段有可能在初始化之后执行。

加载

因为非数组类的加载可以使用自定义的类加载器进行加载,因此该阶段是整个加载过程中可控性最强的阶段。在加载阶段,类需要完成以下三个过程:

  • 通过类的全限定名获取类的二进制字节流(除了从class文件获取字节流,还可以通过网络、zip包、动态生成等多种方式获取)
  • 将字节流的静态存储结构转化成方法区的运行时数据结构
  • 在内存中生成这个类的Class对象,作为方法区该类各种数据结构的访问入口

加载与连接阶段部分内容是交叉进行的,加载部分尚未完成,连接阶段可能已经开始。

验证

验证阶段是为了确保字节流中的数据符合虚拟机的要求,验证部分主要分为以下四个阶段:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

如果运行的代码已经反复使用和验证过,可以使用-Xverify:none参数关闭大部分验证措施,缩短类加载时间。

准备

准备阶段是为类变量(static修饰的变量)分配内存并设置初始值的过程。变量所使用的内存将在方法区中分配(实例变量在对象实例化时会随对象一起分配在堆内存中)。初始值一般情况下是指数据的零值,如:

1
public static int i = 123;

变量i的初始值为0而不是123。(特殊情况:若类变量为常量,初始化时会将其赋指定值)

解析

解析过程是虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用:以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机内存布局无关,引用目标不一定已经加载到内存中。
  • 直接引用:直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用与虚拟机内存布局相关,引用目标在内存中已经存在。

初始化

初始化阶段是类加载的最后一步,将会根据代码中的赋值动作为变量进行赋值。

类加载器与双亲委派模型

类加载器

要明白双亲委派模型首先需要了解类加载器。所谓类加载器的作用就是通过类的全限定名将类的二进制字节流加载到内存中,并生成class对象。
类加载器一般可以分为启动类加载器,扩展类加载器,应用程序类加载器三种,每个加载器的作用如下:

  • 启动类加载器(Bootstamp ClassLoader):负责加载<JAVA_HOME>/lib或者-Xbootclasspath指定的目录下能被虚拟机识别(按文件名识别,名称不符无法被加载)的类库。该启动器由c++所写,无法被java直接引用,随JVM一起启动。
  • 扩展类加载器(Extension ClassLoader):加载<JAVA_HOME>/lib/ext或者被java.ext.dirs系统变量所指定的目录。
  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)上所指定的类库。

双亲委派模型

多个加载器之间的层次结构如下图所示,除了启动类加载器之外,其他加载器都有对应的父类加载器:
paste image

定义

当一个类加载器收到加载请求时,首先会将请求交给父类加载器进行加载,按照上图的层次结构一直到达启动类加载器,只有当父类加载器无法加载时,类加载器自己才会完成这个请求

上述的这个工作过程就称为双亲委派模型

意义

避免内存中出现多份相同的字节码,比如类A,B两个类都引用System类,因为有双亲委派模型的存在,A,B两个类中的System最终都会交给Bootstamp ClassLoader加载,Bootstamp ClassLoader若发现已经加载过System,则会直接返回内存中的对象,不会多次加载。

自定义ClassLoader

查看ClassLoader源码我们可以发现,虚拟机在加载类时会调用loadClassInternal方法,该方法的唯一作用就是调用loadClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// This method is invoked by the virtual machine to load a class.
private Class loadClassInternal(String name)
throws ClassNotFoundException
{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}

在loadClass中首先会判断当前类是否已经加载,已经加载直接返回,没有加载的话则会尝试使用父类加载器进行加载,只有当父类加载器无法完成加载时才会调用本身的findClass方法

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) {
// 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;
}
}

因此在自定义类加载器时只需要覆写findClass即可,这样可以保证自定义的类加载器是符合双亲委派模型的。

验证双亲委派模型

我们可以自定义一个类加载器进行验证,自定义类加载器代码如下:

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
/**
* 加载指定路径下的class文件
*
* @author zhangwl
* @date 2018/3/29 8:43
*/
public class MyClassLoader extends ClassLoader {
private String path;

public MyClassLoader(ClassLoader parent, String path) {
super(parent);
this.path = path;
}

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

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String newName = name;
//class文件路径
path = path + newName.replaceAll("\\.","//") + ".class";
InputStream is = null;
try {
is = new FileInputStream(path);
byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name,bytes,0,bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}

在项目和桌面上各定义一个Person类,包含有say方法。

1
2
3
4
5
6
7
8
9
/**
* @author zhangwl
* @date 2018/3/29 10:03
*/
public class Person {
public void say(){
System.out.println("Hello World Project");
}
}

测试类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author zhangwl
* @date 2018/3/29 9:22
*/
public class Test {
@org.junit.Test
public void MyClassLoaderTest() throws Exception{
MyClassLoader classLoader = new MyClassLoader("C:\\Users\\Administrator\\Desktop\\");
Class cls = classLoader.loadClass("Person");
Method say = cls.getMethod("say");
Object demo = cls.newInstance();
say.invoke(demo);
}
}

可以发现测试类最终结果如下,由于双亲委派模型的存在,自定义的findClass并没有被调用:
mark

我们可以将测试代码稍作修改,显式指定父类加载器为null,

mark

mark

由于无法通过父加载器进行加载,最终调用了我们自定义的findclass方法。

参考