当前位置: 首页 > 科技观察

外婆问我:父母委托的原则是什么?

时间:2023-03-21 19:03:43 科技观察

本文转载自微信公众号《三太子敖丙》,作者:三太子敖丙。转载本文请联系三太子敖丙公众号。我敢打赌你在开发过程中经常会遇到一些类加载问题,比如:ClassNotFoundExceptionCause:java.lang.ClassNotFoundException:Cannotfindclass:com.cc.ANoClassDefFoundErrorCause:java.lang.NoClassDefFoundError:Cannotfindclass:com.cc。两者都与java类加载有关。如果你不了解JVM中类加载的原理,上面的问题会非常令人沮丧。如果运气好在网上找到了解决方案,那也只是暂时解决了问题,在其他场景遇到了还会继续迷茫。.在本文中,我将讲解Java类加载器的双亲委托加载原理,并结合示例程序深入研究类的双亲委托加载机制。类加载的原理大家都很了解。了解了类加载的原理后,遇到上面类似的问题都可以很快解决,在后续的开发中也可以避免类似的问题。什么是Java类加载?java类加载器负责将编译好的Java类文件加载到Java虚拟机(JVM)的运行时数据区,供执行引擎调用。java类加载在JVM架构中的位置如图:原来的jvm架构图没有类加载机制,写好的java程序无法在JVM中运行,所以掌握java类加载非常重要。JVM类加载层次当一个java程序执行时,会启动一个JVM进程。JVM在启动时会做一些初始化操作,比如获取系统参数等,然后创建一个启动类加载器来加载一些必要的JVM运行时。类加载到内存中,同时创建另外两个类加载器,扩展类加载器和系统类加载器。启动类加载器、扩展类加载器和系统类加载器的关系如下图所示:jvmbuilt-inclassLoader**启动类加载器:**java虚拟机启动后创建的第一个类加载器,由C++语言实现,所以我们在java代码中查看它的信息时,看到的都是null。扩展类加载器:由启动类加载器加载,扩展类加载器中parent的值为null(表示指向启动类加载器),继承自URLClassLoader。系统类加载器:由启动类加载器加载,系统类加载期间parent的值设置为上面创建的扩展类加载器,同时继承自URLClassLoader。在代码中,可以通过以下方式查看类加载器中指向的parent:类加载时使用的委托对象,其继承的ClassLoader中定义了parent属性,定义如下。publicabstractclassClassLoader{.....................//TheparentclassloaderfordelegationprivatefinalClassLoaderparent;JVM类加载的默认加载路径每一种类加载器默认都会有自己的加载路径,而启动类加载器、扩展类加载器、系统类加载器的默认加载路径如下图所示:三种类加载器的加载路径如上图所示:1.引导类加载器(BootClassLoader)是用C++编写,负责运行JVM启动时将jdk本身的一些核心类(jar包形式)加载到JVM中,加载时查找资源的路径由只读系统属性指定:"sun.boot.class.path”,一般为:“JAVA_HOME/jre/classes”目录(该目录下只能放class文件,jar包格式文件不生效)。查看启动类加载类加载路径可以通过获取系统属性:“sun.boot.class.path”查看,如图:在lancher中设置启动类加载路径启动类加载器加载路径2、扩展类加载器(ExtClassLoader),负责将系统属性“java.ext.dirs”指向的目录下的类文件(jar包或直接类文件形式)加载到JVM中。比如通常的ext类加载路径是:“$JAVA_HOMEx/jre/lib/ext”。支持在JVM启动前修改路径,运行中修改路径不会生效。扩展类路径中仅支持jar包。查看扩展类加载器的类加载路径,可以获取系统属性:"java.ext.dirs"查看或者升级到URLClassLoader(上面说扩展类加载器继承自URLClassLoader),查看urls属性位于父类URLClassLoader中,如图:扩展类加载器路径3.系统类加载器(AppClassLoader),负责将应用类路径下的类文件(jar包或直接类文件)加载到JVM中.当没有设置classpath路径时,默认加载当前路径下的class文件。查看系统类加载器的类加载路径,可以获取系统属性:"java.class.path"查看或升级到URLClassLoader(据说扩展类加载器继承自URLClassLoader),查看urls属性位于父类URLClassLoader中。勾选一下,如图:系统类加载路径JVM类加载双亲委派机制JVM向虚拟机加载class文件时,默认先使用系统类加载器加载所使用的类,双亲委派加载机制被采纳。所谓双亲委托,顾名思义就是当前类加载器(以系统类加载器为例)在加载一个类时委托给它的双亲(注意这里的双亲指的是所指向的类加载器)类加载器中的父属性)首先进行加载。父类加载器在加载时也委托给自己的parents,如此循环直到某个类加载器没有parents(通常parent为null,即当前parent为扩展类加载器,其parent为Start类loader),然后开始依次在各自的类路径中寻找并加载class类。如下图所示:parentdelegationparentdelegationloadinginstance实例使用JDK版本javaversion"1.8.0_261"Java(TM)SERuntimeEnvironment(build1.8.0_261-b12)JavaHotSpot(TM)64-BitServerVM(build25.261-b12,mixedmode)这个例子涉及两个类:TestMain.java和A.java。期间,TestMain是启动类。在启动类中调用A类中的方法执行并输出,分别输出启动类和依赖类的类加载器信息。类定义如下:A_javaTestMain我们把这两个java文件复制到某个目录下,比如在我本地的E:\java_app目录下,打开windows下的命令行窗口,切换到E:\java_app,对于当前java的编译文件并执行命令javacTestMain.java。此时会在当前目录下产生相应的class文件(这里只需要对TestMain执行编译命令即可,因为TestMain依赖A,所以Jdk编译器会自动先编译依赖的A),如图图:编译命令接下来,我们将观察java类加载机制是如何实现双亲委托加载的。委托给扩展类加载器加载,因为扩展类加载在自己的类路径下,只支持查找jar包的方式,所以我们通过工具将A.class文件打包成A.jar。然后把A.jar放在扩展类加载路径:$JAVA_HOME/jre/lib/ext,同时把A.class文件放在当前目录下。如图:扩展委托在当前目录下仍然保留了A.class文件:E:\java_app,在扩展类加载器路径下多了一个包含A.class的A.jar文件,在当前目录下执行java命令执行TestMain,命令为:javaTestMain,输出如下:Extendeddelegationresults从上图的输出结果可以看出,虽然A类在系统类的加载路径中加载器,由于类加载的委托机制,A首先会由系统类加载器委托其父扩展类加载器加载,而A.class(包含在A.jar中)恰好包含在A的加载路径中扩展类加载器,所以A最终是由扩展类加载器加载的。委托给启动类加载器加载通常情况下,普通类的加载不应该委托给启动类加载器加载,因为前面说过,启动类加载器是C++实现的,是在java虚拟机启动时生成的。在环境中得到的信息都是空的。为了探究类加载的双亲委托机制,本例特意构造一个普通类委托给它加载的场景。在谈到启动类加载器的加载路径时指出,启动类加载器的加载路径由只读系统属性“sun.boot.class.path”指定,只支持加载固定的jar该目录下的文件。在jdk8中,“$JAVA_HOME/jre/classes”目录也是启动类加载器加载的路径(这个路径可能默认不存在,可以手动创建),只能放class文件和jar包文件在此目录中失败。因此,本示例程序将当前目录下的A.class文件复制到启动类加载器的类路径:“$JAVA_HOME/jre/classes”,并保留当前目录下的A.class文件,以及在类路径中扩展类加载A.jar。类存放路径如图:Delegatestartup在当前目录:E:\java_app目录下执行命令运行TestMain,命令为:javaTestMain,输出如下:可以看到委托启动结果从上图的输出来看,虽然类A在系统类加载器的加载路径中也存在于扩展类加载器的加载路径中,但是由于类加载的委托机制,A会先被委托给系统类加载器向其父扩展类加载器进行加载。扩展类加载器会继续委托加载(其实是因为扩展类加载器的父类:启动类加载器为null,此时的委托动作实际上是在启动类加载器的加载路径中寻找类A),最后A的加载由启动类加载完成。双亲委托加载方向在加载类时,类加载器只能递归向上委托给它的父类进行类加载,而不能反向委托父类加载当前类加载器进行类加载。在中国象棋中,棋子过河后的行走轨迹只能向前或左右平移,可以形象地比喻为父类委托类加载的方向性。过河兵比喻当前类加载器委托其父类加载某个类。该类后续依赖的加载与当前类加载器无关。过河后的棋子只能向前走,也就是说当父类加载类的依赖类时,只能继续递归地进行双亲委派。左右平移表示递归父委托加载失败后,父类加载到父类加载器自己的加载路径中。为了说明委托是有方向的,我们继续对上面两个类TestMain.class和A.class进行实验。上面委托示例中我们的场景是:TestMain依赖A,我们通过parent委托加载A。在本次实验中,我们将TestMain委托给parents来加载。参考上面的操作步骤,将TestMain.class放入TestMain.jar中,放在扩展类加载器的加载路径下,TestMain.class在当前目录下,如下图:delegateloadingorder1to切换到当前应用程序目录下,执行java命令运行程序:javaTestMain,执行结果如下:委托序列的执行结果如上图所示,出现错误,TestMain被加载扩展类加载器,但无法加载依赖的A。原因在于上述委托加载是有方向的:1、当运行java命令执行TestMain程序时,系统类加载器准备加载TestMain。根据双亲委托机制,它首先委托给它的双亲加载,最后由双亲扩展类加载。编译器在其加载路径中的TestMain.jar中找到TestMain.class,完成对TestMain的加载。2、TestMain依赖A,此时会根据TestMain加载的类加载器加载A:扩展类加载器,加载方法根据委托机制递归委托给parent加载。扩展类加载器的父类是启动类。如果启动类加载器的加载路径中不存在A,则加载失败。这时扩展类加载器在自己的加载路径中加载A,由于加载路径中没有A.class,所以A.class存在于系统类加载器的加载路径中,但是扩展类加载器会没有返回委托系统类加载器加载,所以直接抛出加载失败异常,出现如上错误。总结本次简单介绍了java类加载在整个JVM中的作用,详细介绍了JVM中启动类加载器、扩展类加载器和系统类加载器的关系,重点介绍了parent的原理类加载的delegationloading,了解了java双亲委托加载的原理后,可以在后续的程序开发设计中掌握更多程序动态设计的高级技巧,开发出更优秀的产品。