本文转载自微信公众号《是的练级攻略》,作者是是。转载本文请联系yes的练级指导公众号。你好,我是。这道面试题来自一群朋友的面试题分享,也就是我组建的面试交流群。其实不止三遍,而是四遍。今天我们就来讨论一下这个面试题,但是在讲双亲委派模型之前,我们得先简单了解下类加载。类加载我们平时写的代码都是保存在一个.java文件中,编译后会生成一个.class文件。该文件存储字节码。如果我们要使用我们的代码,我们必须将它加载到JVM中。当然,加载JVM生成class对象的来源不一定是.class文件,也可以来自网络等,反正只要符合JVM规范即可。类加载的步骤主要分为:加载、链接、初始化。加载其实就是找到字节流,然后加载到JVM中生成类对象。这个阶段就是类加载器派上用场的阶段,后面我们会细说。链接阶段是将生成的类对象集成到JVM中,经过以下三个步骤:验证是检查加载的类是否满足JVM的约束条件,即判断是否合规。准备工作是为加载类的静态变量申请内存空间,并赋初值,比如int类型的初值为0。解析就是将符号引用解析为实际引用。用人的话来说,比如在Yes类中引用了一个类XX,那么Yes类一开始肯定不知道XX类在内存中的地址,所以先换成符号引用,假装知道,然后在类加载解析的时候找到XX类的真实地址,做一个实际的引用。这就是解析的作用。还有一点,虽然解析是放到链接阶段,但是JVM规范并没有要求在链接过程中完成解析。初始化阶段就是给常量字段赋值,然后执行静态代码块,将一堆要执行的静态代码块方法包装到clinit方法中执行。这个方法会被锁定,JVM会保证clinit方法只会被执行一次。所以可以使用一个内部静态类来实现惰性初始化的单例设计模式,同时保证线程安全。这个阶段完成后,类加载过程就ok了,就可以投入使用了。画个图总结一下:在双亲委托模型的加载阶段,需要类加载器将class文件的内容获取到JVM中,生成类对象。那么什么是双亲委派模式呢?一句话,双亲委派模型就是子类加载器先请求父类加载器找到要加载的类,父类继续请求它的父类,直到最顶层,在父类加载器请求的时候类没有找到,子类加载器会尝试加载它,这样一层一层往上爬下。每个类加载器都有一个固定的路径来查找类。JDK8中一共有三个类加载器。BootstrapClassLoader是虚拟机本身的一部分,用C++实现,主要负责加载\lib目录下或-Xbootclasspath指定路径下的文件,其文件名是虚拟机能识别的。它是所有类加载器的父亲。Java实现的扩展类加载器(ExtensionClassLoader),独立于虚拟机,主要负责加载\lib\ext目录或java.ext.dirs系统变量指定路径下的类库.应用类加载器(ApplicationClassLoader),它是用Java实现的,是独立于虚拟机的。它主要负责加载用户类路径(classPath)上的类库。如果我们不实现一个自定义的类加载器,那么这个东西就是我们程序中的默认加载器。为什么会提出双亲委派模式?其实就是为了让基础类能够正确、统一的加载。从上图可以看出,如果你也定义了一个java.lang.Object类,那么请求会通过双亲委托方式委托给启动类加载器,即扫描\lib目录,找到jdk定义的java.lang.Object类来加载,这样你写的java.lang.Object类根本不会被加载,这样可以防止一些程序不小心或故意覆盖基类。至此我们已经了解了什么是双亲委派以及为什么需要双亲委派。接下来我们看一下三个破坏。第一次破坏是在jdk1.2之前。那时候还没有双亲委派模型,但是已经有了一个抽象类ClassLoader,于是有人继承了这个抽象类,重写了loadClass方法,实现了一个自定义的类加载器。1.2中引入了双亲委派模型。为了向前兼容,必须保留loadClass方法以便可以重写它。新建了一个findClass方法供用户改写,呼吁大家不要改写loadClass,改写WritefindClass。这是第一次破坏双亲委派模型,因为双亲委派的逻辑是在loadClass上,但是loadClass是允许改写的。改写后,委托逻辑就可以销毁了。二次断线二次断线是指JNDI、JDBC等的情况。首先你要知道什么是SPI(ServiceProviderInterface),它是面向扩展的,也就是说我定义了一个rule,就是SPI,怎么实现由extender来实现。我们比较熟悉的JDBC就是这样。MySQL有MySQL的JDBC实现,而Oracle有Oracle的JDBC实现。我Java不关心你如何在内部实现它。不管怎样,各位数据库厂商要按照我的做法,让我们Java开发人员可以方便的调用数据库操作,所以在Java核心包中定义了ThisSPI。核心包中的类都是由启动类加载器加载的,但它的手只能接触到\lib或Xbootclasspath指定的路径,不能接触到其他的。JDBC实现类只能在我们自定义的classpath中由应用类加载器加载,所以启动类加载器只能委托子类加载数据库厂商提供的具体实现,违背了自下而上的方式。上面的委托机制。具体解决方法是通过setContextClassLoader()创建线程上下文类加载器,默认为应用类加载器,然后使用Thread.current.currentThread().getContextClassLoader()获取类加载器进行加载。这是第二次违反双亲委派模式。第三次销毁这种销毁是为了满足热部署的需要。不停机更新对企业来说非常重要。毕竟,停机是一件大事。OSGI采用自定义类加载器机制完成模块化热部署,但其实现的类加载机制并没有完全遵循自下而上的委托,同行之间类加载器查找较多。展开了,有兴趣的可以自己研究下。这是第三次毁灭。第四次破坏是在JDK9中引入模块系统之后,类加载器的实现其实经历了一波更新。例如,扩展类加载器改名为平台类加载器,核心类加载器被划分为一些分区。平台类加载器承担了更多的类加载器。上面提到的-Xbootclasspath和java.ext.dirs也是Invalid,rt.jar之类的也被去掉,整理存放在jimage文件中,通过新的JRT文件系统访问。当收到类加载请求时,首先会判断该类是否定义在命名模块中。有定义就自己加载,没有定义就委托给父类。JDK9相关的知识点就不展开了,有兴趣的可以自行查看。所以这是第四次毁灭。其他注意事项首先,虽然是子类父类,但是loader之间的关系不是继承,而是组合。看代码就很清楚了。具体逻辑如下:在JVM中,一个类的唯一性是由类加载器实例和类的全限定名决定的,也就是说,即使是同一个类文件加载的类,加载了不同的类加载器实例,从JVM的角度来看这也是两个类。所以类加载器也有命名空间的作用。我记得这个知识点也是一道面试题~
