面试官:不会有人不懂类加载器与双亲委派模型吧?

开发 前端
类加载器在加载阶段,会将class文件加载进方法区。有关类加载的全过程,可以先参考我的另外一篇文章类的奇幻漂流——类加载机制探秘

[[374028]]

 类加载器在加载阶段,会将class文件加载进方法区。有关类加载的全过程,可以先参考我的另外一篇文章类的奇幻漂流——类加载机制探秘

类加载器的类型

类加载器有以下种类:

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用类加载器(Application ClassLoader)

启动类加载器

内嵌在JVM内核中的加载器,由C++语言编写(因此也不会继承ClassLoader),是类加载器层次中最顶层的加载器。用于加载java的核心类库,即加载jre/lib/rt.jar里所有的class。由于启动类加载器涉及到虚拟机本地实现细节,我们无法获取启动类加载器的引用。

扩展类加载器

它负责加载JRE的扩展目录,jre/lib/ext或者由java.ext.dirs系统属性指定的目录中jar包的类。父类加载器为启动类加载器,但使用扩展类加载器调用getParent依然为null。

应用类加载器

又称系统类加载器,可用通过 java.lang.ClassLoader.getSystemClassLoader()方法获得此类加载器的实例,系统类加载器也因此得名。应用类加载器主要加载classpath下的class,即用户自己编写的应用编译得来的class,调用getParent返回扩展类加载器。

扩展类加载器与应用类加载器继承结构如图所示:

面试官:不会有人不懂类加载器与双亲委派模型吧?

可以看到除了启动类加载器,其余的两个类加载器都继承于ClassLoader,我们自定义的类加载,也需要继承ClassLoader。

双亲委派机制

当一个类加载器收到了一个类加载请求时,它自己不会先去尝试加载这个类,而是把这个请求转交给父类加载器,每一个层的类加载器都是如此,因此所有的类加载请求都应该传递到最顶层的启动类加载器中。只有当父类加载器在自己的加载范围内没有搜寻到该类时,并向子类反馈自己无法加载后,子类加载器才会尝试自己去加载。

ClassLoader内的loadClass方法,就很好的解释了双亲委派的加载模式:

  1. protected Class<?> loadClass(String name, boolean resolve) 
  2.       throws ClassNotFoundException 
  3.   { 
  4.       synchronized (getClassLoadingLock(name)) { 
  5.           //检查该class是否已经被当前类加载器加载过 
  6.           Class<?> c = findLoadedClass(name); 
  7.           if (c == null) { 
  8.             //此时该class还没有被加载 
  9.               try { 
  10.                   if (parent != null) { 
  11.                     //如果父加载器不为null,则委托给父类加载 
  12.                       c = parent.loadClass(namefalse); 
  13.                   } else { 
  14.                      //如果父加载器为null,说明当前类加载器已经是启动类加载器,直接时候用启动类加载器去加载该class 
  15.                       c = findBootstrapClassOrNull(name); 
  16.                   } 
  17.               } catch (ClassNotFoundException e) { 
  18.               } 
  19.  
  20.               if (c == null) { 
  21.                   //此时父类加载器都无法加载该class,则使用当前类加载器进行加载 
  22.                   long t1 = System.nanoTime(); 
  23.                   c = findClass(name); 
  24.                   ... 
  25.               } 
  26.           } 
  27.           //是否需要连接该类 
  28.           if (resolve) { 
  29.               resolveClass(c); 
  30.           } 
  31.           return c; 
  32.       } 
  33.   } 

 为什么要使用双亲委派机制,就使用当前的类加载器去加载不就行了吗?为啥搞得这么复杂呢?

假设现在并没有双亲委派机制,有这样的一个场景:

用户写了一个Student类,点击运行,此时编译完成后,虚拟机开始加载class,该class会由应用加载器进行加载,由于Object类是Student的父类,且双亲委派机制不存在的情况下,应用加载器就会自己尝试加载Object类,但是用户压根没定义Object,即应用加载器无法在加载范围搜寻到该类,所以此时Object类无法被加载,用户写的代码无法运行。

假设该用户自己定义了一个Object类,此时再次运行后,应用类加载器则会正常加载用户定义的Object与Student类。Student类中会调用System.out.print()输出Student对象,此时会由启动类加载器加载System类,在此之前同样也会加载Object类。

此时,方法区中有了两份Object的元数据,Object类被重复加载了!

倘若用户定义的Object类不安全,可能直接造成虚拟机崩溃或者引起重大安全问题。

如果现在使用双亲委派机制,用户虽然自己定义了Object类,可以通过编译,但是永远不会被记载进方法区。

双亲委派机制避免了重复加载,也保证了虚拟机的安全。

自定义类加载器

我们整理ClassLoader里面的流程

  1. loadclass:判断是否已加载,使用双亲委派模型,请求父加载器,父加载器反馈无法加载,因此使用findclass,让当前类加载器查找
  2. findclass:当前类加载器根据路径以及class文件名称加载字节码,从class文件中读取字节数组,然后使用defineClass
  3. defineclass:根据字节数组,返回Class对象

我们在ClassLoader里面找到findClass方法,发现该方法直接抛出异常,应该是留给子类实现的。

  1. protected Class<?> findClass(String name) throws ClassNotFoundException { 
  2.       throw new ClassNotFoundException(name); 
  3.   } 

 到这里,我们应该明白,loadClass方法使用了模版方法模式,主线逻辑是双亲委派,但如何将class文件转化为Class对象的步骤,已经交由子类去实现。对模版方法模式不熟悉的同学,可以先参考我的另外一篇文章模版方法模式

其实源码中,已经有一个自定义类加载的样例代码,在注释中:

  1. class NetworkClassLoader extends ClassLoader { 
  2.         String host; 
  3.         int port; 
  4.  
  5.         public Class findClass(String name) { 
  6.             byte[] b = loadClassData(name); 
  7.             return defineClass(name, b, 0, b.length); 
  8.         } 
  9.  
  10.         private byte[] loadClassData(String name) { 
  11.             // load the class data from the connection 
  12.             
  13.         } 
  14.     } 

 看得出来,如果我们需要自定义类加载器,只需要继承ClassLoader,并且重写findClass方法即可。

现在有一个简单的样例,class文件依然在文件目录中:

  1. package com.yang.testClassLoader; 
  2.  
  3. import sun.misc.Launcher; 
  4.  
  5. import java.io.*; 
  6.  
  7. public class MyClassLoader extends ClassLoader { 
  8.  
  9.     /** 
  10.      * 类加载路径,不包含文件名 
  11.      */ 
  12.     private String path; 
  13.  
  14.  
  15.     public MyClassLoader(String path) { 
  16.         super(); 
  17.         this.path = path; 
  18.     } 
  19.  
  20.     @Override 
  21.     protected Class<?> findClass(String name) throws ClassNotFoundException { 
  22.         byte[] bytes = getBytesFromClass(name); 
  23.         assert bytes != null
  24.         //读取字节数组,转化为Class对象 
  25.         return defineClass(name, bytes, 0, bytes.length); 
  26.     } 
  27.  
  28.     //读取class文件,转化为字节数组 
  29.     private byte[] getBytesFromClass(String name) { 
  30.         String absolutePath = path + "/" + name + ".class"
  31.         FileInputStream fis = null
  32.         ByteArrayOutputStream bos = null
  33.         try { 
  34.             fis = new FileInputStream(new File(absolutePath)); 
  35.             bos = new ByteArrayOutputStream(); 
  36.             byte[] temp = new byte[1024]; 
  37.             int len; 
  38.             while ((len = fis.read(temp)) != -1) { 
  39.                 bos.write(temp, 0, len); 
  40.             } 
  41.             return bos.toByteArray(); 
  42.         } catch (IOException e) { 
  43.             e.printStackTrace(); 
  44.         } finally { 
  45.             if (null != fis) { 
  46.                 try { 
  47.                     fis.close(); 
  48.                 } catch (IOException e) { 
  49.                     e.printStackTrace(); 
  50.                 } 
  51.             } 
  52.             if (null != bos) { 
  53.                 try { 
  54.                     bos.close(); 
  55.                 } catch (IOException e) { 
  56.                     e.printStackTrace(); 
  57.                 } 
  58.             } 
  59.         } 
  60.         return null
  61.     } 
  62.  
  63.     public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { 
  64.         MyClassLoader classLoader = new MyClassLoader("C://develop"); 
  65.         Class test = classLoader.loadClass("Student"); 
  66.         test.newInstance(); 
  67.     } 

 Student类:

  1. public class Student { 
  2.     public Student() { 
  3.         System.out.println("student classloader is" + this.getClass().getClassLoader().toString()); 
  4.     } 

 注意,这个Student类千万不要加包名,idea报错不管他即可,然后使用javac Student.java编译该类,将生成的class文件复制到c://develop下即可。

运行MyClassLoader的main方法后,可以看到输出:


看得出来,Student.class确实是被我们自定义的类加载器给加载了。

破坏双亲委派

从上面的自定义类加载器的内容中,我们应该可以猜到了,破坏双亲委派直接重写loadClass方法就完事了。事实上,我们确实可以重写loadClass方法,毕竟这个方法没有被final修饰。双亲委派既然有好处,为什么jdk对loadClass开放重写呢?这要从双亲委派引入的时间来看:

  • 双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,jdk为了向前兼容,不得已开放对loadClass的重写操作。

当然,也不止这一次对双亲委派模型的破坏,详细的文章可以参考破坏双亲委派模型,里面提到了一个“线程上下文类加载器”,对这个不熟悉的同学可以参考真正理解线程上下文类加载器(多案例分析)(无法放链接,百度搜索)

我们经常用的Tomcat与jdbc,就破坏了双亲委派,碍于文章的篇幅与博主的水平,暂时不在这里讨论破坏的原因,有兴趣的同学可以参考这一篇文章JDBC、Tomcat为什么要破坏双亲委派模型?(无法放链接,百度搜索)

 

责任编辑:姜华 来源: 今日头条
相关推荐

2023-12-06 12:11:43

类加载器双亲委派模型

2023-01-27 23:14:26

Go2兼容性Go1

2023-02-03 07:24:49

双亲委派模型

2022-07-26 19:06:16

Linux命令MacOS

2023-08-04 08:53:42

2009-04-17 15:24:20

人生撤销耍赖

2024-02-20 08:13:35

类加载引用Class

2020-08-03 07:04:54

测试面试官应用程序

2019-03-25 08:47:43

京东腾讯高层

2024-12-04 09:01:55

引导类加载器C++

2021-07-28 10:08:19

类加载代码块面试

2020-11-02 07:02:10

加载链接初始化

2022-03-21 09:05:18

volatileCPUJava

2020-06-22 08:16:16

哈希hashCodeequals

2024-03-27 09:15:27

2024-02-22 15:36:23

Java内存模型线程

2021-02-06 09:21:17

MySQL索引面试

2023-07-13 08:19:30

HaspMapRedis元素

2021-08-04 08:31:10

MySQL数据库日志

2024-06-24 08:24:57

点赞
收藏

51CTO技术栈公众号