详解Class加载过程

简介

探探字节码文件中,我们学习了class文件作为一个二进制字节流,是如何通过二进制的方式表达编程思想的。那么,class文件是由编译器编译得到的,默默的放在硬盘里面,当我们需要运行程序的时候,我们必须将class文件加载进内存中。那么,class文件需要经过一个什么样的过程才能到内存里准备好呢?

本篇文章:我们将讨论class文件作为一个二进制字节流文件,JVM虚拟机是如何对这个文件加载并且准备执行的。

class加载进内存,准备执行的过程中,总共分三大步。第一步是Loading,第二步是Linking,第三步是Intializing。其中Linking又分成三小步,第一小步是Verification (验证),第二小步是Preparation (准备),第三小步是Resolution (解析)

Loading

类加载器

Loading是把一个class文件load装进内存的,在JVM中有一套自定义的类加载器的层次,jvm所有的class都是被类加载器加载到内存的,这个类加载器也叫做ClassLoader。每个类加载器本身也是一个普通的class文件,最终由最高层次的加载器加载进JVM中。

如果,我想知道类是由哪个加载器加载进内存里的,我们只需打印一下这个加载器,如下面代码:

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

public class ClassLoaderT02 {

// Launcher$AppClassLoader类和Launcher$ExtClassLoader都是在rt.jar文件下,被Boostrap加载器加载
public static void main(String[] args) {
// Boostrap加载器主要加载jre/lib/rt.jar包下的类
System.out.println(System.class.getClassLoader());
System.out.println(java.lang.Object.class.getClassLoader());

// Translator 属于jre/lib/ext/*.jar目录下
System.out.println(Translator.class.getClassLoader());
System.out.println(Translator.class.getClassLoader().getClass().getClassLoader());

System.out.println(ClassLoaderT02.class.getClassLoader());
System.out.println(ClassLoaderT02.class.getClassLoader().getParent());
System.out.println(ClassLoaderT02.class.getClassLoader().getParent().getParent());
System.out.println(ClassLoaderT02.class.getClassLoader().getClass().getClassLoader());
}

/*output
null
null
sun.misc.Launcher$ExtClassLoader@1cd072a9
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1cd072a9
null
null
*/
}

这里为什么会打印空呢?原因为:Boostrap加载类,是由C++实现的,java里没有对应实现,所以为空。

由程序可以证明

  1. AppClassLoader、ExtClassLoader是sun.misc.Launcher的内部类。
  2. AppClassLoader、ExtClassLoader都是由Boostrap加载器加载的。
  3. AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BoostrapClassLoader。

Launcher是ClassLoader一个包装启动类,通过这个类我们可以看到每个类加载器的范围

1
2
3
4
5
6
// BoostrapClassLoader
private static String bootClassPath = System.getProperty("sun.boot.class.path");
// ExtClassLoader
String var0 = System.getProperty("java.ext.dirs");
// AppClassLoader
final String var1 = System.getProperty("java.class.path");

我们可以测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ClassLoaderT03 {


// 检查jdk自带的三种加载器分别负责加载哪些jar包
public static void main(String[] args) {
System.out.println("====================== boostrap ClassLoder ======================");
String boostrapPath = System.getProperty("sun.boot.class.path");
// System.lineSeparator()为系统换行符
boostrapPath = boostrapPath.replaceAll(":", System.lineSeparator());
System.out.println(boostrapPath);

System.out.println("====================== extention ClassLoder ======================");
String extensionPath = System.getProperty("java.ext.dirs");
extensionPath = extensionPath.replaceAll(":", System.lineSeparator());
System.out.println(extensionPath);

System.out.println("====================== application ClassLoder ======================");
String applicationPath = System.getProperty("java.class.path");
applicationPath = applicationPath.replaceAll(":", System.lineSeparator());
System.out.println(applicationPath);
}
}

输出为:

双亲委派

双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程。双亲委派,主要出于安全来考虑。

加载的过程如图所示:

查找的加载器路线是:

自定义的ClassLoader—> AppClassLoader—>ExtClassLoader—> BoostrapClassLoader

重复的动作是:findInCache -> parent.loadClass -> findClass()

委派的方向刚好相反:

BoostrapClassLoader—>ExtClassLoader—>AppClassLoader—>自定义的ClassLoader

其实,这个可以看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
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); // findInCache
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); // parent.loadClass
} 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); // findClass

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

任何一个class,都会先从自定义的classLoader开始,因为内部维护了缓存,先从缓存中查找,如果没有,就委托给父加载器,父加载器也是重复同样的动作,直到最顶层的BoostrapClassLoader。如果父加载器缓存没有,class文件也不在自己负责的文件范围内,就重新委托给当前加载器。

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

当前加载器会从自己负责加载的文件夹查找,如果有,就加载。如果没有,就抛出ClassNotFoundException

整个加载过程如图

那么,为什么类的加载机制要搞一套双亲委派模型出来呢?

我们可以假设,如果任何一个class可以通过自定义的ClassLoader记载进内存里,那么,我们就可以把java.lang.String交给自定义的加载器加载,我们可以动手脚,通过将程序打包给客户,用户使用后可以把密码发给我自己。

如果有了双亲委派,就会先去上面查有没有加载过,上面有加载过就直接返回,不会让自定义加载器加载。

自定义加载器

通过以上分析,我们很容易的知道自定义加载器只需要

  1. 继承ClassLoader父类
  2. 重写模版方法findClass
    1. 调用defineClass

代码如下

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
public class T006_MSBClassLoader extends ClassLoader {

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// File f = new File("c:/test/", name.replace(".", "/").concat(".class"));
File f = new File("/Users/alongso_pro/Desktop/class/Hello.class");

try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;

while ((b=fis.read()) !=0) {
baos.write(b);
}

byte[] bytes = baos.toByteArray();
baos.close();
fis.close();//可以写的更加严谨

return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name); //throws ClassNotFoundException
}

public static void main(String[] args) throws Exception {
ClassLoader l = new T006_MSBClassLoader();
Class clazz = l.loadClass("com.mashibing.jvm.Hello");
Class clazz1 = l.loadClass("com.mashibing.jvm.Hello");

System.out.println(clazz == clazz1);
//
// Hello h = (Hello)clazz.newInstance();
// h.m();

// System.out.println(h.getClass().getClassLoader());
// System.out.println(h.getClass().getClassLoader().getParent());

System.out.println(getSystemClassLoader());
}
}

Linking

Verification

验证文件是否符合JVM规定。如果加载进来的文件首4个字节不是CAFE BABE开头,在这个步骤就被拒绝掉了。

Preparation

把class文件中的静态成员变量赋默认值,不是初始值。比如static int i=8,注意在这个步骤并不是直接把i赋成8,而是先赋值成0。

Resolution

将类、方法、属性等符号引用解析为直接引用。class文件常量池里面用到的符号引用,要给它转换为直接内存地址,直接可以访问到的内容。

Initializing

调用类初始化代码 <clinit>,给静态成员变量赋初始值。

LazyLoading

懒加载,严格上叫做懒初始化,也叫延迟初始化。jvm规范没有严格定义何时加载,但虚拟机的实现是懒加载,就是我什么时候需要这个类的时候才会去加载这个类,并不是我用到jar包里的一个类,就将jar包所有的类都加载进来。

我们来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ClassLoadingProcedureT00 {
public static void main(String[] args) {
// 当引用静态变量时,对T初始化
System.out.println(T.count);
}

}
// 初始化时,先对static变量赋默认值,再执行构造函数,置初始值。
class T {

//public static T t = new T();
public static int count = 2;
private int m = 8;

private T() {
count++;
System.out.println("T Construct !!,count=" + count);
}
}

该程序输出的结果为

1
/*output 2  */

由该程序可得

  1. 引用静态变量时,会对T初始化,赋初始值。

注意:初始化指的是是否会调用类初始化代码<clinit>,给静态成员变量赋初始值,不要和实例构造函数<init>混淆了。

思考:如果把11行放开,会输出什么呢?如果把12行放在11行前面,又会输出什么?

初始化的五种情况

在周老师的《深入理解java虚拟机》明确指出,懒初始化 在下面五种情况下才能触发:

  1. new getstatic putstatic invokestatic指令,访问final变量除外。
  2. java.lang.reflect对类进行反射调用时。
  3. 初始化子类的时候,父类首先初始化。
  4. 虚拟机启动时,被执行的主类必须初始化。
  5. 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化。

思考:我们可以逐步注释以下代码,看看会输出什么?

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
public class T008_LazyLoading { 
public static void main(String[] args) throws Exception {
// P p;
X x = new X();
// System.out.println(P.i); //常量放在mataspace
// System.out.println(X.j); //初始化 new getstatic putstatic invokestatic
// Class clazz=Class.forName("com.selfstudy.jvm.classloader.T008_LazyLoading$X");
// clazz.newInstance();
}

public static class P {
final static int i = 8;
static int j = 9;
static {
System.out.println("P static");
}

P() {
System.out.println("P construct!");
}
}

public static class X extends P {
static {
System.out.println("X static");
}
X() {
System.out.println("X Construct!");
}
}
}

总结

  1. classloading是使用双亲委派机制从上到下从下到上从上找向下委托。主要出于安全考虑。
  2. LazyLoading五种情况在《深入理解java虚拟机》里有详细的列出来。
  3. ClassLoaer的源码:findInCache -> parent.loadClass ->findClass()
  4. 自定义加载器
    1. extends ClassLoader
    2. overwrite findclass()—>defineClass(bytes[])