双亲委派加载模型
为什么需要双亲委派加载模型
主要是为了安全, 避免用户恶意加载破坏 JVM 正常运行的字节码文件, 比如说加载一个自己写的 java.util.HashMap.class. 这样就有可能造成包冲突问题.
类加载器种类
启动类加载器: 用于加载 jdk 中 rt.jar 的字节码文件
扩展类加载器: 用于加载 jdk 中 / jre/lib/ext 文件夹下的字节码文件
应用程序类加载器: 加载 classPath 下的字节码文件
自定义类加载器: 用户在程序中自己定义的加载器
源码分析
- 1,ClassLoader.loadClass()
- 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);
- // 如果这个 Class 对象还没有被加载, 下面就准备加载
- if (c == null) {
- long t0 = System.nanoTime();
- try {
- // 查看当前类加载器有没有父类加载器
- if (parent != null) {
- // 父类加载器来加载字节码文件
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
- // 如果父类加载器也没有加载这个 Class 对象, 就由自己来加载
- 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;
- }
- }
不遵守双亲委派加载模型的例子
双亲委派加载模型仅仅是一个约定, 后面实现类加载器时, 是可以不遵守这个约定. ClassLoader 是在 JDK1.0 的时候就设计好的, 而双亲委派加载模型在 JDK1.2 引入的. 所以, 有些机制是没有遵守这个约定的. 比如: Service Provider Interface 机制的 JDBC 就没有遵守这个约定.
1, 为什么 JDBC 无法遵守这个约定?
JDBC 是 SPI 机制的一个例子, JDK 定义了 java.sql.Connection 核心接口, 后续 MySQL,Oracle 为其提供实现类. 在运行中是通过 java.sql.DriverManager 来获取指定实现类的实例. 这里需要明白三个问题:
java.sql.DriverManager
是在 rt.jar 中, 由核心类加载器加载的;
第三方所提供 Collection 的实现类都是在 classpath 中;
类中方法想加载新的字节码文件时, 其初始类加载器就是当前这个类的定义类加载器;
也就是说当 JVM 在 java.sql.DriverManager 类的 getConnection() 方法中获取 Collection 实现类的字节码时, 当前类的定义类加载器是启动类加载器, 而按照约定启动类加载器是不允许加载 classpath 下的字节码. 所以, JDBC 就无法遵守这个约定.
2,JDBC 是如何解决上面的问题的?
为了解决这个, java 在线程中放入一个类加载器 Thread.currentThread().getContextClassLoader(); 而这个类加载器可以是随意的. 比如你想加载 classpath 包下的字节码文件, 只需要设置当前线程的类加载器为应用程序类加载器即可.
JVM 类加载过程
JVM 本质的工作就是读取字节码文件, 执行字节码文件中的指令. 其中 JVM 将读取字节码文件的过程称为 JVM 类加载过程.
JVM 读取的字节码文件将放在方法区里;
JVM 类加载机制分为五个部分: 加载, 验证, 准备, 解析, 初始化. 如下图所示:
一, Loading: 加载
这一步是将 JVM 外的字节码文件加载到 JVM 内部方法区中的 Class 对象.
JVM 可以通过几种方式来加载外部的字节码文件?
从本地读字节码文件;
从网络读取字节码文件;
通过动态生成的字节码文件;
初始类加载器和定义类加载器
由于双亲委派加载模型的存在, 一个 Class 对象的初始类加载器 initiating class loader 和定义类加载器 defining class loader 有可能不是同一个.
初始类加载器: 它是指让 JVM 加载这个字节码文件
定义类加载器: 它是真正调用 defineClass 方法, 将字节码转换成 Class 对象
java 在判断 instanceof 时, 只有类名, defining class loader 都相等, 才表示是同一个类的实例.
Class.getClassLoader() 得到的是定义类加载器
相关实验代码
1, 验证使用不同 ClassLoader 加载字节码文件
- // 这种方法是不遵守双亲委派加载模型的约定
- public class ClassLoaderLoading {
- public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
- // 这个 Class 对象是由当前方法的类加载器加载
- Class c1 = MiniJVM.class;
- Class c2 = new MyClassLoader().loadClass("com.github.hcsp.MiniJVM");
- // 使用 c2 创建一个 MiniJVM 实例
- Object o = c2.getConstructor().newInstance();
- System.out.println(o instanceof MiniJVM);
- MiniJVM demo = (MiniJVM) o;
- }
- private static class MyClassLoader extends ClassLoader {
- @Override
- public Class<?> loadClass(String name) throws ClassNotFoundException {
- if (name.contains("MiniJVM")) {
- try {
- byte[] bytes = Files.readAllBytes(new File("target/classes/com/github/hcsp/MiniJVM.class").toPath());
- return defineClass(name, bytes, 0, bytes.length);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- } else {
- return super.loadClass(name);
- }
- }
- }
- }
2, 实现一个遵守双亲委派加载模型的类加载器
- public class ClassLoaderLoading {
- public static void main(String[] args) throws ClassNotFoundException {
- Class c1 = MiniJVM.class;
- Class c2 = new MyClassLoader(ClassLoader.getSystemClassLoader()).loadClass("com.github.hcsp.MiniJVM");
- System.out.println("c2 =" + c2);
- }
- private static class MyClassLoader extends ClassLoader {
- public MyClassLoader(ClassLoader systemClassLoader) {
- super(systemClassLoader);
- }
- @Override
- public Class<?> loadClass(String name) throws ClassNotFoundException {
- // 加载你想让这个类加载器加载的字节码文件
- if (name.contains("MiniJVM")) {
- try {
- byte[] bytes = Files.readAllBytes(new File("target/classes/com/github/hcsp/MiniJVM.class").toPath());
- return defineClass(name, bytes, 0, bytes.length);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- } else {
- // 其他的字节码文件交由父类加载器加载
- return super.loadClass(name);
- }
- }
- }
- }
二, Linking: 链接
当一个. java 文件编译成. class 文件时, 里面含有一个符号引用, 比如 / java/utils/HashMap.Linking 是指将这符号引用与具体的 class 对象链接起来.
每个字节码结构都有一个运行时常量池, 它会存储每个符号引用和所对应的具体对象, 以此实现链接.
Verification: 验证字节码的正确性
Preparation: 为 static 成员赋默认初始值
Resolution: 解析当前字节码里包含的其他符号引用
三, Initializing
执行初始化方法. 比如下面的四个虚拟机指令: new,getstatic,putstatic,invokestatic
原博客地址 http://wangjie.natappvip.cc/
来源: https://www.cnblogs.com/fourther/p/12687964.html