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


    类加载是什么

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

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

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

    运行 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" 临时指定。

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

    1. 验证
    2. 准备
    3. 解析
    4. 初始化
    5. 使用
    6. 卸载

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

    类加载器

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

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

    1. package dccmmtop;
    2. public Class User {
    3. public static void main(String[] args) {
    4. System.out.println("hello");
    5. System.out.println(User.Class.getClassLoader());
    6. }
    7. }

    如图可以看到类加载器的名字叫做 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 中:

    1. protected Class<?> loadClass(String name, boolean resolve)
    2. throws ClassNotFoundException
    3. {
    4. synchronized (getClassLoadingLock(name)) {
    5. // First, check if the Class has already been loaded
    6. Class<?> c = findLoadedClass(name);
    7. if (c == null) {
    8. long t0 = System.nanoTime();
    9. try {
    10. if (parent != null) {
    11. c = parent.loadClass(name, false);
    12. } else {
    13. c = findBootstrapClassOrNull(name);
    14. }
    15. } catch (ClassNotFoundException e) {
    16. // ClassNotFoundException thrown if Class not found
    17. // from the non-null parent Class loader }
    18. if (c == null) {
    19. // If still not found, then invoke findClass in order
    20. // to find the Class. long t1 = System.nanoTime();
    21. c = findClass(name);
    22. // this is the defining Class loader; record the stats
    23. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
    24. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    25. sun.misc.PerfCounter.getFindClasses().increment();
    26. }
    27. }
    28. if (resolve) {
    29. resolveClass(c);
    30. }
    31. return c;
    32. }
    33. }

    上面代码块中的弟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 集合称作缓存

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

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

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

    BootStrapClassLoader

    引导类加载器

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

    ExtClassLoader

    扩展类加载器

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

    AppClassLoader

    应用程序加载器

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

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

    1. package dccmmtop;
    2. import sun.misc.Launcher;
    3. import java.net.URL;
    4. public Class User {
    5. public static void main(String[] args) {
    6. System.out.println(String.Class.getClassLoader()); // null
    7. System.out.println(com.sun.crypto.provider.DESKeyFactory.Class.getClassLoader().getClass().getName()); //sun.misc.Launcher$ExtClassLoader
    8. System.out.println(User.Class.getClassLoader().getClass().getName()); // sun.misc.Launcher$AppClassLoader
    9. System.out.println();
    10. System.out.println("bootstrapLoader加载以下文件:");
    11. URL[] urls = Launcher.getBootstrapClassPath().getURLs();
    12. for (int i = 0; i < urls.length; i++) {
    13. System.out.println(urls[i]);
    14. }
    15. System.out.println();
    16. System.out.println("extClassloader加载以下文件:");
    17. System.out.println(System.getProperty("java.ext.dirs"));
    18. System.out.println();
    19. System.out.println("appClassLoader加载以下文件:");
    20. System.out.println(System.getProperty("java.Class.path"));
    21. }
    22. }

    输入如下:

    1. null // 因为调用了 c++ 实现。无法获取到java对象
    2. sun.misc.Launcher$ExtClassLoader
    3. sun.misc.Launcher$AppClassLoader
    4. the bootstrapLoader : null
    5. the extClassloader : sun.misc.Launcher$ExtClassLoader@77459877
    6. the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
    7. bootstrapLoader加载以下文件:
    8. file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/resources.jar
    9. file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/rt.jar
    10. file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/sunrsasign.jar
    11. file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jsse.jar
    12. file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jce.jar
    13. file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/charsets.jar
    14. file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jfr.jar
    15. file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/Classes
    16. extClassloader加载以下文件:
    17. C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
    18. appClassLoader加载以下文件:
    19. 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;...省略

    为什么使用双亲委派机制

    1. 沙箱安全机制: 自己写的java.lang.String.Class类不会被加载,这样便可以防止核心 API库被随意篡改

    2. 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

    全盘负责委托机制

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

    自定义类加载器

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

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

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

    目录如下:

    App.java:

    1. import java.io.FileInputStream;
    2. import java.lang.reflect.Method;
    3. public class App {
    4. static class MyClassLoader extends ClassLoader {
    5. private String classPath;
    6. public MyClassLoader(String classPath) {
    7. this.classPath = classPath;
    8. }
    9. // 从磁盘加载文件
    10. private byte[] loadByte(String name) throws Exception {
    11. name = name.replaceAll("\\.", "/");
    12. FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
    13. int len = fis.available();
    14. byte[] data = new byte[len];
    15. fis.read(data);
    16. fis.close();
    17. return data;
    18. }
    19. // 重写
    20. protected Class<?> findClass(String name) throws ClassNotFoundException {
    21. try {
    22. byte[] data = loadByte(name);
    23. // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。
    24. return defieClass(name, data, 0, data.length);
    25. } catch (Exception e) {
    26. e.printStackTrace();
    27. throw new ClassNotFoundException();
    28. }
    29. }
    30. }
    31. public static void main(String args[]) throws Exception {
    32. // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
    33. MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盘创建
    34. // 创建 io/dc 几级目录,将User类的复制类User.class丢入该目录
    35. Class clazz = classLoader.loadClass("io.dc.User");
    36. Object obj = clazz.newInstance();
    37. // 使用反射调用 User 类的 sout 方法
    38. Method method = clazz.getDeclaredMethod("sout", null);
    39. method.invoke(obj, null);
    40. System.out.println(clazz.getClassLoader().getClass().getName());
    41. }
    42. }

    打破双亲委派机制

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

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

    1. import java.io.FileInputStream;
    2. import java.lang.reflect.Method;
    3. public class App {
    4. static class MyClassLoader extends ClassLoader {
    5. private String classPath;
    6. public MyClassLoader(String classPath) {
    7. this.classPath = classPath;
    8. }
    9. // 从磁盘加载文件
    10. private byte[] loadByte(String name) throws Exception {
    11. name = name.replaceAll("\\.", "/");
    12. FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
    13. int len = fis.available();
    14. byte[] data = new byte[len];
    15. fis.read(data);
    16. fis.close();
    17. return data;
    18. }
    19. protected Class<?> findClass(String name) throws ClassNotFoundException {
    20. try {
    21. byte[] data = loadByte(name);
    22. // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。
    23. return defineClass(name, data, 0, data.length);
    24. } catch (Exception e) {
    25. e.printStackTrace();
    26. throw new ClassNotFoundException();
    27. }
    28. }
    29. protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    30. synchronized (getClassLoadingLock(name)) {
    31. // First, check if the class has already been loaded
    32. Class<?> c = findLoadedClass(name);
    33. if (c == null) {
    34. // If still not found, then invoke findClass in order
    35. // to find the class.
    36. long t1 = System.nanoTime();
    37. if (name.startsWith("io.dc")) {
    38. // 直接查找, 限定包名
    39. c = findClass(name);
    40. } else {
    41. // 其他包中的类还是使用双亲委派机制
    42. // 否则会报找不到 Object
    43. c = this.getParent().loadClass(name);
    44. }
    45. // this is the defining class loader; record the stats
    46. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    47. sun.misc.PerfCounter.getFindClasses().increment();
    48. }
    49. if (resolve) {
    50. resolveClass(c);
    51. }
    52. return c;
    53. }
    54. }
    55. }
    56. public static void main(String args[]) throws Exception {
    57. // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
    58. MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盘创建
    59. // 创建 io/dc 几级目录,将User类的复制类User.class丢入该目录
    60. Class clazz = classLoader.loadClass("io.dc.User");
    61. Object obj = clazz.newInstance();
    62. // 使用反射调用 User 类的 sout 方法
    63. Method method = clazz.getDeclaredMethod("sout", null);
    64. method.invoke(obj, null);
    65. System.out.println(clazz.getClassLoader().getClass().getName());
    66. }
    67. }
  • 相关阅读:
    ideal 同一项目启动多实列
    数仓分层设计
    【技术积累】Python中的Pandas库【二】
    数据采集时使用HTTP代理IP效率不高怎么办?
    【九】http服务器开发--高并发的实现http服务器示例
    SSM整合shiro
    动态链接库(一)--动态链接库简介
    查看双翌视觉软件版本号
    AI办公自动化:批量根据Excel表格内容制作Word文档
    使用composer安装ffmpeg的步骤
  • 原文地址:https://blog.csdn.net/jcc4261/article/details/128091399