前言
之前详细介绍了 Java 类的整个加载过程 (类加载机制详解). 虽然, 篇幅较长, 但是也不要被内容吓到了, 其实每个阶段都可以用一句话来概括.
1) 加载: 查找并加载类的二进制字节流数据.
2) 验证: 保证被加载的类的正确性.
3) 准备: 为类的静态变量分配内存, 并设置默认初始值.
4) 解析: 把类中的符号引用转换为直接引用.
5) 初始化: 为类的静态变量赋予正确的初始值.
当然, 要想掌握类加载机制, 还是需要去深入研究的.(好吧, 说了一句正确的废话) 因为其中, 有很多知识点也是面试中常问的. 比如, 我之前去面试的时候, 面试官就问到了一个和类初始化相关的问题. 就是给一段代码, 有父子类关系, 父子类中包含静态代码块, 构造代码块, 普通代码块, 构造函数等, 然后让判断代码最终的执行顺序.(可自行思考一下, 具体内容细节暂时不做扩展)
类加载器
终于来到了本文的主题 -- 类加载器和双亲委派机制.
在《深入理解 Java 虚拟机》中, 对于类加载器的定义是这样的:
虚拟机设计团队把类加载阶段中的 "通过一个类的权限定名来获取描述此类的二进制字节流" 这个动作放到 Java 虚拟机外部去实现, 以便让应用程序自己决定如何去获取所需要的类. 实现这个动作的代码模块称为 "类加载器".
简单来说, 类加载器的作用就是去加载 class 类的二进制字节流的.
类加载器有以下三种:
1) 启动类加载器 (Bootstrap ClassLoader), 或者叫根加载器. 这个类加载器主要是去加载你在本机配置的环境变量 Java_Home/jre/lib 目录下的核心 API, 如 rt.jar
2) 扩展类加载器 (Extension ClassLoader). 这个加载器负责加载 Java_Home/jre/lib/ext 目录下的所有 jar 包.
3) 应用程序类加载器 (Application ClassLoader). 这个加载器加载的是你的项目工程的 ClassPath 目录下的类库. 如果用户没有自定义自己的类加载器, 这个就是程序默认的类加载器.
另外, 如果有需要的话, 用户也可以自定义自己的类加载器 (去继承 ClassLoader 类).
我们也可以通过代码把类加载器打印出来:
- public class TestClassLoader {
- public static void main(String[] args) {
- Object obj = new Object();
- System.out.println(obj.getClass().getClassLoader());
- TestClassLoader t = new TestClassLoader();
- System.out.println(t.getClass().getClassLoader());
- System.out.println(t.getClass().getClassLoader().getParent());
- System.out.println(t.getClass().getClassLoader().getParent().getParent());
- }
- }
打印结果:
- null
- sun.misc.Launcher$AppClassLoader@58644d46
- sun.misc.Launcher$ExtClassLoader@6d6f6e28
- null
注意, 上面第一行和第四行的 null 此处可不是空的意思, 它代表的是启动类加载器. 因为启动类加载器是用 C++ 代码来实现的, 严格来说不属于 Java 类, 所以 Java 代码访问不到, 故返回 null. 第二行是应用程序类加载器, 第三行是扩展类加载器.
双亲委派机制
在介绍双亲委派机制之前, 先观察一下以下代码能否正确运行:
- // 自己定义的一个 java.lang 包
- package java.lang;
- public class String {
- public static void main(String[] args) {
- String s = new String();
- System.out.println(s);
- }
- }
以上代码, 编译没有任何问题, 但是运行时, 却报错:
为什么提示在 java.lang.String 类中找不到 main 方法呢, 我这明明不是定义了吗? 其实, 问题的关键就在于类加载遵循双亲委派机制.
类加载器有以下这样的层次关系:
当一个类在加载的时候, 都会先委派它的父加载器去加载, 这样一层层的向上委派, 直到最顶层的启动类加载器. 如果顶层无法加载 (即找不到对应的类), 就会一层层的向下查找, 直到找到为止. 这就是类的双亲委派机制.
这样做有什么好处呢? 这就相当于维护了一个有优先级的层级关系, 即总是从最顶层的父加载器开始加载. 这就如同, 你工作中遇到了问题需要向上反馈, 比如先反馈给小组长, 然后小组长反馈给上级经理, 最后经理反馈给 boss. 然后 boss 感觉这问题太简单了不需要他亲自出手, 让经理自己解决吧, 然后经理又向下交给小组长. 小组长一看, 这问题不算难, 人也比较热心, 于是就帮你把问题解决了.(可能例子不是太恰当哈, 意思理解即可)
到此, 我们就明白了为什么上边的代码会报错. 因为双亲委派机制的存在, 去加载我们自己定义的 "java.lang.String" 类的时候, 会最终委派到顶层的启动类加载器, 然后找到了 rt.jar 包下的 "java.lang.String". 找到之后, 就直接加载 rt.jar 包的 String 类 (也就是我们经常使用的那个字符串类), 不再去向下查找, 也就加载不了我们自定义的 String 类了. 由于, rt.jar 包下的 String 类中确实没有 main 方法, 所以才会有以上的报错信息.
我们可以试想一下, 如果没有双亲委派机制的存在, 那我这段代码是不是就可以执行成功了. 如果这样的话, 岂不是说明我可以随意覆盖 rt.jar 包中的类 (如 String,Integer 类等). 这样的话将会使程序陷入混乱, Java 核心包中的类的安全也无法保证.
来源: https://www.cnblogs.com/starry-skys/p/12299803.html