在冯诺依曼的计算机模型中,任何程序都要加载到内存才能与CPU进行交流。字节码.class文件同样需要加载到内存中,才可以实例化。
其中,类加载器ClassLoader的使命就是加载.class文件到内存中。
在加载类时,使用的是Parents Delegation Model,即双亲委派模型。
Java类加载器是一个运行时核心基础设施模块,如下图,主要是在启动之初进行类的加载(Load)、链接(Link)、初始化(Init)。
第一步,Load阶段。
读取.class文件流,并转换为特定的数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例。
第二步,Link阶段。
包括验证、准备和解析三个步骤。
- 验证:是更详细地校验,比如final是否合规、类型是否正确、静态变量是否合理等。
- 准备:为静态变量分配内存,设定默认初始值。
- 解析:解析类和方法之间相互引用正确性,完成内存布局。
第三步,Init阶段。
执行类构造器的<clinit>方法。
类加载器
类加载器类似于原始部落结构,存在权利等级制度。
最高一层是Bootstrap ClassLoader,它是在JVM启动时创建的,是最根基的类加载器,负责装载最核心的Java类,比如Object、System、String等。由C++编写。
第二层是Extension ClassLoader(JDK9之后变为了Platform ClassLoader,平台类加载器)。用以加载扩展的系统类,比如XML、加密、压缩等相关类。Java编写。
第三层是Application ClassLoader,应用类加载器,主要是加载用户自定义classpath下的类。
双亲委派模型
结合上图,我们来看一下ClassLoader的loadClass()方法:
1 /**
2 @Param resolve 是否链接
3 @Param name 类名
4 */
5 protected Class<?> loadClass(String name, boolean resolve)
6 throws ClassNotFoundException
7 {
8 synchronized (getClassLoadingLock(name)) {
9 // 首先检查该类是否已经被加载过
10 Class<?> c = findLoadedClass(name);
11 if (c == null) {
12 long t0 = System.nanoTime();
13 try {
14 // 如果父加载器不为空,就用它的父加载器去加载该类
15 if (parent != null) {
16 c = parent.loadClass(name, false);
17 } else {
18 // 到这里的话就是bootstrapClassLoader
19 // 这是一个本地方法,使用bootstrapClassLoader去加载该类
20 // 如果加载不了,就返回空
21 c = findBootstrapClassOrNull(name);
22 }
23 } catch (ClassNotFoundException e) {
24
25 }
26
27 // 如果返回空了,证明其父类加载器不能加载该类
28 if (c == null) {
29
30 long t1 = System.nanoTime();
31 // 使用自己进行加载
32 c = findClass(name);
33
34 // this is the defining class loader; record the stats
35 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
36 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
37 sun.misc.PerfCounter.getFindClasses().increment();
38 }
39 }
40 // 如果resove为true,就链接
41 if (resolve) {
42 resolveClass(c);
43 }
44 return c;
45 }
46 }
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载过的类。
如果底层的类加载器想要加载一个未知类,要非常有礼貌的向上级询问:”请问,这个类已经加载了吗?“,被询问的高层类首先要回答自己一个问题:我是否加载过此类?如果答案为否,那么又向上询问,直到Bootstrap ClassLoader为止,如果它也未加载过并且并加载,就会逐层交给下一层类加载器,反应在源码中就是第28行逐层递归退出。
我们看一下Bootstrap所有已经加载的类库:
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
Arrays.asList(urls).forEach(System.out::println);
结果:
file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/resources.jar
file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/rt.jar
file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/sunrsasign.jar
file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/jsse.jar
file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/jce.jar
file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/charsets.jar
file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/jfr.jar
file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/classes
Bootstrap的加载路径是可以追加的,不建议修改或删除原有的加载路径。通过-Xbootclasspath/a:参数可以增加类加载路径。
如果想在启动时观察加载了哪个jar包中的类,可以增加-XX:+TraceClassLoading参数,此参数在解决类冲突时非常实用。
什么时候需要自定义类加载器?
(1)隔离加载类。在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。
(2)修改类加载方式。出了Bootstrap外,其他的加载并非都要引入。
(3)扩展加载源。比如从数据库、网络进行加载。
(4)防止源码泄露。Java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义。
实现自定义类加载器的步骤:继承ClassLoader,重写findClass()方法,调用defineClass()方法。下面是一个例子:
public class MyClassLoader extends ClassLoader {
private String path;
private String name;
public MyClassLoader(String path, String name) {
this.path = path;
= name;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = null;
try {
data = getClassBytes(name);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// 获取字节码流
private byte[] getClassBytes(String className) throws IOException {
InputStream in = null;
ByteArrayOutputStream baos = null;
try {
int len = 0;
byte[] bytes = new byte[1024];
in = new FileInputStream(new File(path + className + ".class"));
baos = new ByteArrayOutputStream();
while ((len = in.read(bytes)) != -1) {
baos.write(bytes, 0, len);
baos.flush();
}
} catch (Exception e) {
}finally {
baos.close();
in.close();
}
return baos.toByteArray();
}
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader classLoader = new MyClassLoader("E:\\","my classloader");
Class clz = classLoader.findClass("Dog");
Field[] fields = clz.getDeclaredFields();
System.out.println(Arrays.toString(fields));
}