java中类加载与双亲委派机制详解

目录
  • 类加载是什么
  • 类加载器
  • 双亲委派机制
    • BootStrapClassLoader
    • ExtClassLoader
    • AppClassLoader
    • 为什么使用双亲委派机制
    • 全盘负责委托机制
  • 自定义类加载器
  • 打破双亲委派机制

类加载是什么

把磁盘中的java文件加载到内存中的过程叫做类加载

当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM. 有如下 User 类

package dc.dccmmtop;
public Class User {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

运行 java dc.dccmmtop.User 时, 先要找到 User.Class 文件,查找全路径就是 Class_PATH + {{package name}},对于User类来说,就是 {$Class_APTH}/dc/dccmmtop.User.Class

假如 User.java 在F:\code, 并且不在Class_PATH 下,可以通过 java -Classpath "F:\code" 临时指定。

加载类之后还有后续的步骤:

  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

这篇文章主要来讲讲类加载

类加载器

不了解类加载机制的,可能就认为,只需找到java文件所在的磁盘位置,然后进行一次读文件的操作不就完成了加载嘛,其实远非如此。

总有一个加载类的工具,这个工具叫做类加载器,在java代码中可以通过如下方式获取当前类的类加载器是什么

package dccmmtop;  

public Class User {
    public static void main(String[] args) {
        System.out.println("hello");
        System.out.println(User.Class.getClassLoader());
    }
}

如图可以看到类加载器的名字叫做 AppClassLoader

我们全局搜索一下这个类,会发现在 sun.misc.Launcher.java 文件中找到。

那么这个AppClassLoader 本身也是一个 java 文件,它又是什么时候被加载并初始化的呢?

我们滚动到文件顶部,看到 Launcher 类的构造方法部分:

标记1 和标记2 实现了一个单例模式,在5 处获取到了 AppClassLoader 实例。也就是说在某一个地方通过调用 Launcher 类中的 getLauncher() 方法,会得到 AppClassLoader 实例, 那么 getLauncher() 方法又是在哪里调用的呢?追踪到这里已经无法在java代码中找到上一步了,其实这个方法是jvm (c++实现)调用的,如下图:

以上就是类加载的主要步骤了。下面看一下双亲委派机制

双亲委派机制

我们继续看AppClassLoader 实例化的过程:

在5处,实例化了一个AppClassLoader的对象,同时传进去了一个参数 var1, 这个 var1 是另外一个类加载器ExtClassLoader , 我们在进入 getAppClassLoader 方法看一看是怎么实现的:

先看一下 几个ClassLoad的继承关系:

有上面的继承关系图可以看出来,AppClassLoader 和 ExtClassLoader 都是从 ClassLoader 继承来的。

在 Launcher() 中可知,调用 AppClassLoader.getAppClassLoader() 方法时, 把 ExtClassLoader 的实例作为参数传递进来,最终到4这一步,作为 var2 参数,调用父类的构造方法,继续追踪父类的构造方法直到 ClassLoader :

在 ClassLoader 构造方法中,维护了一个 parent 变量,到此我们知道了 AppClassLoader 中 parent 变量保存的是 ExtClassLoader的实例, 如下图表示

继续看Launcher 构造方法: 

loadClass() 方法将 Class 文件加载到jvm中,我们跟踪一下这个方法,会发现最后会调到 根类ClassLoader 中:

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);
        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            }  

            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;
    }
}

上面代码块中的弟6行,findLoadedClass() , 先从已加载到的类集合中查找有没有这个类,如果有的话,直接返回,没有再进行下一步, findLoadedClass 方法源码如下:

到 native finnal Class<?> findLoadedClass0(String name); 这里已经无法在向后追踪了,看到 naive ,要明白 使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用.

此时 User.Class 是第一次加载,AppClassLoader 中肯定无法在已加载的集合中找到,所以继续向下走到第 10,11 行. 上面已经分析过,AppClassLoader 中的 parent 是 ExtClassLoader , 所以在11行由 ExtClassLoader 的实例执行 laodClass 方法。 ExtClassLoader 没有覆写根类ClassLoader 的loaderClass 方法,所以也会到这里,只不过 ExtClassLoader 的 parent 是 NUll, 会走到13行,调用findBootstrapClassOrNull() 方法,再看一下这个方法的实现:

会发现这个方法也是C++实现的,虽然我们无法看到源码,但是根据注释可以知道,这个是保存了启动类加载器加载过的类。

到此为止,我们已经见识过3中不同的类加载器了:

  • AppClassLoader
  • ExtClassLoader
  • BootStrapClassLoader

我们先不管这个后面两个类加载器是什么, 假定他们也找不到 User.Class. 继续向下看:

执行到第21行findClas()这里,再看源码

在A-2 这一步,ucp 其实保存的就是当前 ClassLoader 的类加载路径,就不再展开。要记住此时的 ClassLoader 是 ExtClassLoader, 假如仍然找不到User.Class 会执行到 A-3.然后返回到 loadClass 方法中, 此时 c 是空,继续执行到33行,返回到 AppClassLoader 调用 parent.getAppClassLoader 处,在 AppClassLoader 实例的范围下继续向后执行,然后再继续调用 findClass 方法,如果在AppClassLoader的类加载路径中找到User.Class 文件,就会 执行 defindClass(name,res) 方法去加载类文件了。

整个过程用文字描述起来比较复杂,来张图就很清楚了,为什么叫做双亲委派: 

把 loadedClassList 集合称作缓存:

  • 先在 AppClassLoader 中缓存中找,如果找不到向 ExtClassLoader 找,如果能找到,直接返回
  • 在 ExtClassLoader 中缓存找,如果找不到向 BootStrapClassLoader 找,如果能找到,直接返回
  • 在 BootStrapClassLoader 找,如果找不到, 在 ExtClassLoader 类路径集合中找,
  • 如果在 ExtClassLoader 类路径集合找不到,在 AppClassLoader 类路径集合找
  • 如果在 AppClassLoader 类路径集合中能找到,加载该类,并放入缓存。找不到则报错

双亲指的是 ExtClassLoader 和 BootStrapClassLoader, AppClassLoader 先不加载,而是向上让其“父”加载,父加载不到时,自己再加载。这里的父不是父类,而是调用层级的关系。

是时候介绍一下 这三个类加载器

BootStrapClassLoader

引导类加载器

负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等

ExtClassLoader

扩展类加载器

负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包

AppClassLoader

应用程序加载器

负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类

我们可以写代码验证一下:

package dccmmtop;
import sun.misc.Launcher;
import java.net.URL;
public Class User {
    public static void main(String[] args) {
        System.out.println(String.Class.getClassLoader()); // null
        System.out.println(com.sun.crypto.provider.DESKeyFactory.Class.getClassLoader().getClass().getName()); //sun.misc.Launcher$ExtClassLoader
        System.out.println(User.Class.getClassLoader().getClass().getName()); // sun.misc.Launcher$AppClassLoader
        System.out.println();
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i]);
        }
        System.out.println();
        System.out.println("extClassloader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println();
        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.Class.path"));
    }
}

输入如下:

null // 因为调用了 c++ 实现。无法获取到java对象
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader

the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@77459877
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2

bootstrapLoader加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/Classes

extClassloader加载以下文件:
C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

appClassLoader加载以下文件:
C:\Program Files\Java\jdk1.8.0_261\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\jaccess.jar;...省略

为什么使用双亲委派机制

  • 沙箱安全机制: 自己写的java.lang.String.Class类不会被加载,这样便可以防止核心 API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

全盘负责委托机制

“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类
所依赖及引用的类也由这个ClassLoder载入

自定义类加载器

从上述源码的描述可知,类加载器的核心方法是 findClass , 和 defineClass 。

defindClass 将class文件从磁盘加载文件到内存,defineClass 开始解析class文件: 

所以自定义类加载器只需继承 ClassLoader,然后从写 findClass 文件就行了:

目录如下: 

App.java:

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class App {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        // 从磁盘加载文件
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        // 重写
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。
                return defieClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
    }
    public static void main(String args[]) throws Exception {
        // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盘创建
        // 创建 io/dc 几级目录,将User类的复制类User.class丢入该目录
        Class clazz = classLoader.loadClass("io.dc.User");
        Object obj = clazz.newInstance();
        // 使用反射调用 User 类的 sout 方法
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

打破双亲委派机制

经过上面的源码分析发现,主要是 ClassLoader 类中的laodClass 方法来实现的双亲委派机制,自己不加载而是先让其父加载。

所以直接复写 loadClass 方法即可,不再指定父级加载,当前类直接加载,如下:

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class App {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        // 从磁盘加载文件
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        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);
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    if (name.startsWith("io.dc")) {
                        // 直接查找, 限定包名
                        c = findClass(name);
                    } else {
                        // 其他包中的类还是使用双亲委派机制
                        // 否则会报找不到 Object 类
                        c = this.getParent().loadClass(name);
                    }
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }

    public static void main(String args[]) throws Exception {
        // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盘创建
        // 创建 io/dc 几级目录,将User类的复制类User.class丢入该目录
        Class clazz = classLoader.loadClass("io.dc.User");
        Object obj = clazz.newInstance();
        // 使用反射调用 User 类的 sout 方法
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

到此这篇关于java中类加载与双亲委派机制详解的文章就介绍到这了,更多相关java类加载内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解Java类加载器与双亲委派机制

    目录 引子 了解.class文件 类加载的过程 类加载器 与 双亲委派机制 ClassLoader 自定义类加载器 编写一个自定义的类加载器 为什么我们这边要打破双亲委派机制 自定义类加载器时,如何打破双亲委派机制 SPI机制 与 线程上下文类加载器 JDBC Tomcat SpringBoot Starter 尾语 引子 大家想必都有过平时开发springboot 项目的时候稍微改动一点代码,就得重启,就很烦 网上一般介绍 2种方式 spring-boot-devtools,或者通过JRebe

  • Java虚拟机类加载器之双亲委派机制模型案例

    1. 双亲委派模型是什么? 当某个类加载器需要加载某个.class字节码文件时,它首先把这个任务委托给它的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类. 2. 双亲委派模型的工作原理? 1.如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去执行: 2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器:(每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶

  • Java虚拟机之对象创建过程与类加载机制及双亲委派模型

    目录 一.对象的创建过程: 1.对象的创建过程: 2.对象的访问方式: 二.类加载机制: 2.1.加载阶段: 2.2.验证阶段: 2.3.准备阶段: 2.4.解析阶段: 2.5.初始化: 2.5.1.类的主动引用: 2.5.2.类的被动引用: 2.5.3.()方法的特点: 三.类加载器与双亲委派模型: 3.1.JVM 的类加载器: 3.2.双亲委派模型: 3.2.1.双亲委派模型的工作原理: 3.2.2.双亲委派模型的优点: 3.3.类加载器源码:loadClass() 3.4.如何破坏双亲委派

  • java中类加载与双亲委派机制详解

    目录 类加载是什么 类加载器 双亲委派机制 BootStrapClassLoader ExtClassLoader AppClassLoader 为什么使用双亲委派机制 全盘负责委托机制 自定义类加载器 打破双亲委派机制 类加载是什么 把磁盘中的java文件加载到内存中的过程叫做类加载 当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM. 有如下 User 类 package dc.dccmmtop; public Class User { publi

  • Java虚拟机之双亲委派机制详解

    目录 一.原理 二.作用 三.沙箱安全机制 四.补充内容 总结 Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象.而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式. Tips:如果在工程目录的src下新建一个名为 java.lang 的包,在其中新建一个类名String的类,这个类能够正常运行吗? 程序代码如下: package java.lang;

  • Java并发中的Fork/Join 框架机制详解

    什么是 Fork/Join 框架 Fork/Join 框架是一种在 JDk 7 引入的线程池,用于并行执行把一个大任务拆成多个小任务并行执行,最终汇总每个小任务结果得到大任务结果的特殊任务.通过其命名也很容易看出框架主要分为 Fork 和 Join 两个阶段,第一阶段 Fork 是把一个大任务拆分为多个子任务并行的执行,第二阶段 Join 是合并这些子任务的所有执行结果,最后得到大任务的结果. 这里不难发现其执行主要流程:首先判断一个任务是否足够小,如果任务足够小,则直接计算,否则,就拆分成几个

  • Java多线程高并发中的Fork/Join框架机制详解

    1.Fork/Join框架简介 Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出.Fork/Join 框架要完成两件事情: Fork:把一个复杂任务进行分拆,大事化小 :把一个复杂任务进行分拆,大事化小 Join:把分拆任务的结果进行合并 在 Java 的 Fork/Join 框架中,使用两个类完成上述操作: ForkJoinTask: 我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务.该类提供了

  • Java程序执行过程及内存机制详解

    本讲将介绍Java代码是如何一步步运行起来的,其中涉及的编译器,类加载器,字节码校验器,解释器和JIT编译器在整个过程中是发挥着怎样的作用.此外还会介绍Java程序所占用的内存是被如何管理的:堆.栈和方法区都各自负责存储哪些内容.最后用一小块代码示例来帮助理解Java程序运行时内存的变化. Java程序执行过程 步骤 1: 写源代码,源代码将以.java的文件格式保存在电脑硬盘中. 步骤 2: 编译器(compiler)检查是否存在编译期错误(例如缺少分号,关键字拼写错误等).若通过检测,编译器

  • java中Executor,ExecutorService,ThreadPoolExecutor详解

    java中Executor,ExecutorService,ThreadPoolExecutor详解 1.Excutor 源码非常简单,只有一个execute(Runnable command)回调接口 public interface Executor { /** * Executes the given command at some time in the future. The command * may execute in a new thread, in a pooled thre

  • java 中maven pom.xml文件教程详解

    maven pom.xml文件教程详解,具体内容如下所示: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.x

  • Java中Exception和Error的区别详解

    世界上存在永远不会出错的程序吗?也许这只会出现在程序员的梦中.随着编程语言和软件的诞生,异常情况就如影随形地纠缠着我们,只有正确的处理好意外情况,才能保证程序的可靠性. java语言在设计之初就提供了相对完善的异常处理机制,这也是java得以大行其道的原因之一,因为这种机制大大降低了编写和维护可靠程序的门槛.如今,异常处理机制已经成为现代编程语言的标配. 今天我要问你的问题是,请对比Exception和Error,另外,运行时异常与一般异常有什么区别? 典型回答 Exception和Error都

  • Java SpringMVC拦截器与异常处理机制详解分析

    目录 拦截器(interceptor)的作用 拦截器快速入门 案例:用户登录权限控制 拦截器方法说明 SpringMVC异常处理 异常处理的思路 异常处理两种方式 拦截器(interceptor)的作用 Spring MVC的拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理. 将拦截器按一定的顺序联结成一条链,这条链称为拦截器链(Interceptor Chain).在访问被拦截的方法或字段时,拦截器链中的拦截器就会按其之前定义的顺序被调用.拦截器也是AOP思

随机推荐