文章目录
  1. JVM学习03-类加载机制
  2. 1. 类加载器简介
  3. 2. class加载验证流程
    1. 2.1 类加载
    2. 2.2 链接
    3. 2.3 初始化
  4. 3. 类的加载器
    1. 3.1 父类委托机制
    2. 3.2 ClassLoader中的一些方法
    3. 3.3 父类委托的缺陷和解决
  5. 4. 自定义类加载器

[TOC]

JVM学习03-类加载机制

1. 类加载器简介

之前有讲述过 JVM 的启动流程,在JVM找到配置文件对JVM进行初始化的时候,将会生成最基础的类加载器(c/c++语言编写)BootStrap。
在说classLoader前先看一段程序:

1
2
3
4
5
6
7
8
9
ClassLoader classLoader = this.getClass().getClassLoader();
System.out.println("当前的ClassLoader : " + classLoader);
System.out.println("父ClassLoader : " + classLoader.getParent());
System.out.println("曾ClassLoader : " + classLoader.getParent().getParent());

// 结果:
// 当前的ClassLoader : sun.misc.Launcher$AppClassLoader@2a788b76
// 父ClassLoader : sun.misc.Launcher$ExtClassLoader@500c05c2
// 曾ClassLoader : null

可以看出来jdk默认当前应用程序的classLoader为AppClassLoader,然后往上的父ClassLoader是ExtClassLoader,然后是null。

那么为什么最顶层的是null,这也就是BootStrap并不是java语言编写的,而是一个顶级的ClassLoader。当JVM启动Bootrap之后,Bootrap就会随之启动ExtClassLoader和AppClassLoader,并完成父子继承关系,最后由AppClassLoader加载主类的字节码,寻找到main函数进行启动。大致如图:

  1. BootStrapClassLoader:jre路径/lib/rt.jar中的字节码,或者通过 -Xbootclasspath 参数指定路径的下的字节码。
  2. ExtClassLoader : jre路径/lib/ext目录下 *.jar 的字节码。
  3. AppClassLoader: 加载用户classpath下的字节码。

除了 BootStrapClassLoader 不能直接被程序员使用外,其他的类加载器都能被程序使用和修改。

2. class加载验证流程

要让主类中的main函数运行,当然少不了的就是字节码(class)的加载。这个也是class装载验证流程中的第一步类加载。然后第二步是链接最后是进行初始化

2.1 类加载

首先肯定是读取class文件的二进制流。由于Class等类的信息的存储在方法区中的,所以进而转化为方法区的数据结构,为了能够被使用,最后在java堆中生成代表这个字节码的java.lang.Class对象。

2.2 链接

在链接中又分为验证,准备和解析等几个步骤。

(1)验证:验证字节码肯定是为了确保字节码的正确性,比如文件是否以0xCAFEBABE开头,版本是否正确等等。这个过程需要对字节码中的内容做大量的验证,诸如文件的格式,元数据,字节码验证,符合引用验证等等。
(2)准备:之前在讲述java成员初始化一节中提到,在初始化之后首先需要对类进行加载。所以在这一步就是对类中的成员进行内存的分配。例如对于

1
public static int i = 1;

先对i进行内存的分配,然后再赋上初始值为0。注意是准备阶段是0,然后准备阶段完成之后的类的初始化中,才会执行语句赋值为1。

这里在补充一个知识点:编译时常量和运行时常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
public final static int i = 2;
// 对于final static类型, 在准备阶段就会被赋值为2.
// 因为i的值不能修改, 所以在编译的时候, i的值就已经确认了
// 所以对于final修饰变量称为编译时常量.
// ------- 举个面试题
byte b1=1,b2=2,b3,b6;
final byte b4=4,b5=6;
b6=b4+b5;
b3=(b1+b2);
System.out.println(b3+b6);
// 该题目在第四行会报错,原因是(b1 + b2) 会自动将类型提升为int型
// 那为毛第三行不报错呢? 原因是b4和b5带有final类型, 编译器会立即知道他们的值,然后对b4+b5进行优化, 代码其实就变成了
// b6 = 10; 所以不会报错.

(3)解析:在解析阶段主要是将符号引用转换为直接引用。符号引用简单来说就是一个字符串,而这个字符串所引用内容的信息必须能够唯一标识一个类、字段和方法等。比如对于一个类的符号引用,必须给出类的全名,如java.lang.Object。而直接引用就是引用某个对象的一个指针或者地址偏移量,是一定存在的引用对象。

2.3 初始化

类的初始化,包括static的初始化和非static的初始化,详细参考:java变量的初始化顺序

3. 类的加载器

3.1 父类委托机制

由上面的类加载器的继承结构可以看出,每个加载器都负责加载自己负责的区域。从java1.2开始,类加载的过程引入了父类委托的机制,这种机制更好的保证了java平台的安全性。
原则:类的寻找是由下往上的,类的加载是由上往下的。

假设平台只有Bootstrap,ExtClassLoader和AppClassLoader三个加载器,当用户类路径下一个类如MyTest类被加载的时候,AppClassLoader首先查看该类是否已经被加载了,如果有就返回该类的字节码,否则询问父亲ExtClassLoader是否已经加载了。同样ExtClassLoader先查看是否自己已经加载了MyTest,如果有就返回,否则继续问Bootstrap。当然由于是在类路径下,Bootstrap也没有加载,也没有父亲可问。这个时候,他就会查看自己是否能够加载MyTest,当然由于在用户路径下(所以不能加载),然后告诉ExtClassLoader说我不能加载。然后同样ExtClassLoader也不能加载并告诉AppClassLoader也不能加载,最后AppClassLoader在查看该类的路径,对该类进行加载。这个就是类的寻找是由下往上的,加载是由上往下的。

实现父类委托的关键代码(ClassLoader):

3.2 ClassLoader中的一些方法

1
2
3
4
5
6
7
8
9
10
11
12
// 载入并返回一个Class
public Class<?> loadClass(String name) throws ClassNotFoundException

// 定义一个类,不公开调用, jdk提供加载字节码的唯一入口,不能覆盖
// 可从磁盘读取字节码
protected final Class<?> defineClass(byte[] b, int off, int len)

// loadClass回调该方法,自定义ClassLoader的推荐做法
protected Class<?> findClass(String name) throws ClassNotFoundException

// 寻找已经加载的类
protected final Class<?> findLoadedClass(String name)

3.3 父类委托的缺陷和解决

由于类加载器父类委托的存在,就会导致父类加载器无法加载子类加载器所加载的类。换句话说,比如Java的SPI机制(Service Provider Interface
)
中,类的接口是定义在rt.jar包中的,而具体的实现是由指定厂商提供,是定义的AppClassLoader中的,如果rt.jar中有一个类需要返回该接口的一个实现,那么就需要加载获取AppClassLoader中这个接口实现类的字节码。
按照父类委托机制,rt.jar的类加载器是无法访问子加载器加载的字节码的,那么该如何解决。
这个就引入了一个线程上下文类加载器的概念,基本思想是给顶层类加载传入底层类加载器的实例。

1
2
3
4
// 设置线程上下文类加载器
Thread.setContextClassLoader(ClassLoader cl)
// 获取线程上下文类加载器
Thread.getContextClassLoader()

这样,顶层的加载器通过线程的上下文就能访问到底层类加载,从而可以获取底层加载器所加载的字节码。
打破这种默认的父类委托机制模式的应用如Tomcat的WebappClassLoader,OSGi的ClassLoader根据需要自由加载Class等等。
由于父类委托的核心代码是在loadClass方法中实现的,所以当然也可以通过覆盖loadClass方法来打破父类委托的机制,比如:

1
2
3
4
5
6
7
8
9
10
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// First, check if the class has already been loaded
// 在findClass中实现类的定义defineClass装载
Class re=findClass(name);
if(re==null){
System.out.println(“无法载入类:”+name+“ 需要请求父加载器");
return super.loadClass(name,resolve);
}
return re;
}

4. 自定义类加载器

通过自定义类加载器我们可以对手动生成的字节码进行加载,可以实现热部署等等一些功能。
相信大家经常使用tomcat的热部署,那么关于自定义类的热部署可参照博文:

《Java Class热替换》

其他博文参考:
《Java Classloader机制解析》
《翻译:走出类加载器迷宫》

文章目录
  1. JVM学习03-类加载机制
  2. 1. 类加载器简介
  3. 2. class加载验证流程
    1. 2.1 类加载
    2. 2.2 链接
    3. 2.3 初始化
  4. 3. 类的加载器
    1. 3.1 父类委托机制
    2. 3.2 ClassLoader中的一些方法
    3. 3.3 父类委托的缺陷和解决
  5. 4. 自定义类加载器