想聊Java的类加载机制就离不开Java类加载器,这是Java语言的一个很重要的创新点,曾经也是Java流行的重要原因。当初引入这个机制是为了满足Java Applet开发的需求,简单而言,就是为了能够执行从从远程下载过来的的Java类,JVM咬咬牙引入了Java类加载机制,后来的基于jvm的动态部署,插件化开发包括大家热议的热修复(热修复其实也有不基于ClassLoader的解决方案,有兴趣请看我的热修复初探),总之很多后来的技术都源于在JVM中引入了类加载器。
JVM:很惭愧,就做了一点微小的工作,谢谢大家。
加载器
好了,讲完了ClassLoader的来由,接下来可以正是介绍一下类加载器。如你所知,当你写完了一个.java文件的时候,编译器会把他编译成一个由字节码组成的class文件,当程序运行时,JVM会首先寻找包含有main()方法的类,把这个class文件中的字节码数据读入进来,转化成JVM中运行时对应的Class对象。执行这个动作的,就叫类加载器。
- ClassLoader:是Java层几乎所有类加载器的父类,它定义了加载器的基本行为和加载动作
分类
类加载器分为可以大致分为:
-
Bootstrap ClassLoader(启动类加载器)
- 这个类加载器负责将一些核心的,被JVM识别的类加载进来,用C++实现,与JVM是一体的。
-
Extension ClassLoader(扩展类加载器)
- 这个类加载器用来加载 Java 的扩展库
-
Applicaiton ClassLoader(应用程序类加载器)
- 用于加载我们自己定义编写的类
-
User ClassLoader (用户自己实现的加载器)
- 当实际需要自己掌控类加载过程时才会用到,一般没有用到。
与之配套的加载机制就是“双亲委派模型”:
双亲委派模型
先看看Java类加载器的体系结构:
类加载逻辑代码:
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 {
//如果class没有被加载且已经设置parent,那么请求其父加载器加载
if (parent != null) {
/**
*注意当这里调用parent.loadClass()方法找不到Class时会抛出ClassNotFoundException异常,但是该异常是被捕获的
*/
c = parent.loadClass(name, false);
} else {
//如果没有设定parent类加载器,则寻找BootstrapClss并尝试使用Boot loader加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
/**
*如果当前这个loader所有的父加载器以及顶层的Bootstrap ClassLoader都不能加载待加载的类
*那么则调用自己的findClass()方法来加载
*/
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.先检查需要加载的类是否已经被加载,如果没有被加载,则委托父加载器加载,父类继续检查,尝试请父类加载,这个过程是从下-------> 上;
- 2.如果走到顶层发现类没有被加载过,那么会从顶层开始往下逐层尝试加载,这个过程是从上 ------> 下;
需要注意的几个问题:
- 1,双亲XX 这种说法是有问题的,因为Java世界一直是单亲家庭
- 2,事实上加载器之间不是通过继承,而是通过组合的方式来实现整个加载过程,即每个加载器都持有上层加载器的引用,所以父加载器是一种笼统的说法。
这里必须要提一提JVM如何判定两个类你是否相等:
- JVM除了比较类是否相等还要比较加载这两个类的类加载器是否相等,只有同时满足条件,两个类才能被认定是相等的。
接下来问题来了,为什么双亲委派模型要有三层加载器而不是一层?
实际上,三层类加载器代表了JVM对于待加载类的三个信任层次,当需要加载一个全限定名为java.lang.Object的类时,JVM会首先信任顶层的引导类加载器,即优先用这个加载器尝试加载,如果不行,JVM会选择继续信任第二层的拓展类加载器,往下,知道三层都无法加载,JVM才会选择信任开发者自己定义的加载器。这种”父类“优先的加载次序有效的防止了恶意代码的加载。
总结
总而言之,双亲委派模型有效解决了以下问题:
- 每一个类都只会被加载一次,避免了重复加载
- 每一个类都会被尽可能的加载(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它)
- 有效避免了某些恶意类的加载(比如自定义了Java。lang.Object类,一般而言在双亲委派模型下会加载系统的Object类而不是自定义的Object类)
tips:可以说双亲委派模型主要是为了维护Java类加载的安全,防止恶意加载,与此配套的还有命名空间出有效的隔离,命名空间的作用抽象理解就是
- 竖直方向上,父加载器中加载的类对于所有子加载器可见
- 水平方向上,子类之间各自加载的类对于各自是不可间的(达到隔离效果)
基本上,日常的开发使用的都是使用系统提供的类加载器依照“双亲委派模型”来加载的,开发者基本接触不到加载过程。但是当你要动态加载自己的外部的类的时候,比如从网络上下载的class文件,就需要自定义classloader来实现加载过程。
在Android中,QQZone团队提出的基于Dex分包的热修复解决方案就属于加载外部的类,本来应当由开发者自己实现classloader来实现加载过程,但是Android本身已经为我们封装好了一个classloader,就是DexClassLoader(贴心~~)
事实上,如今Java中很多插件化开发,动态部署,热修复等动态技术都是基于Java的类加载器来展开的。因此,我才会想专门用一篇文章总结Java的类加载器和加载机制。后面我会找时间基于HotFix详细的分析其中的类加载过程。毕竟理论总要落实到代码才会让人印象深刻。