Java类加载器与双亲委派机制怎么应用


这篇文章主要讲解了“Java类加载器与双亲委派机制怎么应用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java类加载器与双亲委派机制怎么应用”吧!大家想必都有过平时开发springboot 项目的时候稍微改动一点代码,就得重启,就很烦网上一般介绍 2种方式spring-boot-devtools,或者通过JRebel插件 来实现”热部署”热部署就是当应用正在运行时,修改应用不需要重启应用。其中spring-boot-devtools其实是自动重启,主要是节省了我们手动点击重启的时间,不算真正意义上的热部署。JRebel插件啥都好,就是需要收费但如果平时我们在调试debug的情况下,只是在方法块内代码修改了一下,我们还得重启项目,就很浪费时间。这个时候我们其实可以直接build ,不重启项目,即可 实现热部署。我们先来写一个例子演示一下:结果:name:zj weight: 100修改代码,然后直接build项目,不重启项目,我们再请求这个测试接口:神奇的一幕出现了,结果为:name:ming weight: 300当我们修改.java文件,只需重新生成对应的.class文件,就能影响到程序运行结果, 无需重启,Why? 背后JVM的操作原理且看本文娓娓道来。首先我们得先了解一下 什么是.class文件举个简单的例子,创建一个Person类:我们执行javac命令,生成Person.class文件然后我们通过vim 16进制打开它不同的操作系统,不同的 CPU 具有不同的指令集,JAVA能做到平台无关性,依靠的就是 Java 虚拟机。.java源码是给人类读的,而.class字节码是给JVM虚拟机读的,计算机只能识别 0 和 1组成的二进制文件,所以虚拟机就是我们编写的代码和计算机之间的桥梁。虚拟机将我们编写的 .java 源程序文件编译为 字节码 格式的 .class 文件,字节码是各种虚拟机与所有平台统一使用的程序存储格式,class文件主要用于解决平台无关性的中间文件在之前的一篇文章谈谈JAVA中对象和类、this、super和static关键字中,我们知晓 Java 是如何创建对象的虽然我们写的时候是简单的一句,但是JVM内部的实现过程却是复杂的:将硬盘上指定位置的Person.class文件加载进内存执行main方法时,在栈内存中开辟了main方法的空间(压栈-进栈),然后在main方法的栈区分配了一个变量zhang。执行new,在堆内存中开辟一个 实体类的 空间,分配了一个内存首地址值调用该实体类对应的构造函数,进行初始化(如果没有构造函数,Java会补上一个默认构造函数)。将实体类的 首地址赋值给zhang,变量zhang就引用了该实体。(指向了该对象)类加载过程其中 上图步骤1 Classloader(类加载器) 将class文件加载到内存中具体分为3个步骤:加载、连接、初始化类的生命周期一般有如下图有7个阶段,其中阶段1-5为类加载过程,验证、准备、解析统称为连接类的生命周期1.加载加载阶段:指的是将类对应的.class文件中的二进制字节流读入到内存中,将这个字节流转化为方法区的运行时数据结构,然后在堆区创建一个java.lang.Class 对象,作为对方法区中这些数据的访问入口相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是我们最可以控制的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义类加载器来完成加载。这个我们文章后面再详细讲2.验证验证阶段:校验字节码文件正确性。这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。这部分对开发者而言是无法干预的,以下内容了解即可验证阶段大致会完成4个阶段的检验动作:文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。符号引用验证:确保解析动作能正确执行。验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。3.准备准备阶段:为类变量(static 修饰的变量)分配内存,并将其初始化为默认值注意此阶段仅仅是为类变量 即静态变量分配内存,并将其初始化为默认值举个例子,在这个准备阶段:注意:valFin是被final static修饰的常量在 **编译 **的时候已分配好了,所以在准备阶段 此时的值为5,所以在这个阶段也不会初始化!4.解析解析阶段:是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。这个阶段了解一下即可5.初始化直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。初始化阶段 是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控 制。Java程序对类的使用方式可分为两种:主动使用被动使用。一般来说只有当对类的首次主动使用的时候才会导致类的初始化,所以主动使用又叫做类加载过程中“初始化”开始的时机。类实例初始化方式,主要是以下几种:1、创建类的实例,也就是new的方式2、访问某个类或接口的静态变量,或者对该静态变量赋值3、调用类的静态方法4、反射(如Class.forName("com.test.Person"))5、初始化某个类的子类,则其父类也会被初始化6、Java虚拟机启动时被标明为启动类的类(JavaTest),还有就是Main方法的类会 首先被初始化这边就不展开说了,大家记住即可6.使用当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码7.卸载当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象,最后负责运行的JVM也退出内存在如下几种情况下,Java虚拟机将结束生命周期执行了System.exit()方法程序正常执行结束程序在执行过程中遇到了异常或错误而异常终止由于操作系统出现错误而导致Java虚拟机进程终止上文类加载过程中,是需要类加载器的参与,类加载器在Java中非常重要,它使得 Java 类可以被动态加载到 Java 虚拟机中并执行那什么是类加载器?通过一个类的全限定名来获取描述此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例Java虚拟机支持类加载器的种类:主要包括3中:引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用类加载器(系统类加载器,AppClassLoader),另外我们还可以自定义加载器-用户自定义类加载器引导类加载器(Bootstrap ClassLoader):BootStrapClassLoader是由c++实现的。引导类加载器加载java运行过程中的核心类库JRE\lib\rt.jar,sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar以及存放 在JRE\classes里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List等扩展类加载器(Extension ClassLoader):它用来加载/jre/lib/ext目录以及java.ext.dirs系统变量指定的类路径下的类。应用类加载器(AppClassLoader):它主要加载应用程序ClassPath下的类(包含jar包中的类)。它是java应用程序默认的类加载器免费云主机域名。其实就是加载我们一般开发使用的类用户自定义类加载器:用户根据自定义需求,自由的定制加载的逻辑,只需继承应用类加载器AppClassLoader,负责加载用户自定义路径下的class字节码文件线程上下文类加载器:除了以上列举的三种类加载器,其实还有一种比较特殊的类型就是线程上下文类加载器。ThreadContextClassLoader可以是上述类加载器的任意一种,这个我们下文再细说我们来看一个例子:结果:sun.misc.Launcher
ExtClassLoader@5caf905d null结果显示分别打印应用类加载器、扩展类加载器和引导类加载器由于 引导类加载器 是由c++实现的,所以并不存在一个Java的类,因此会打印出null我们还可以看到结果里面打印了sun.misc.Launcher,这个是什么东东?其实Launcher是JRE中用于启动程序入口main()的类,我们看下Launcher的源码:其中loader = AppClassLoader.getAppClassLoader(extcl);的核心方法源码如下:通过以上源码我们可以知晓:Launcher的ClassLoaderBootstrapClassLoader,在Launcher创建的同时,还会同时创建ExtClassLoader,AppClassLoader(并设置其parent为extClassLoader)。其中代码中 “sun.boot.class.path”是BootstrapClassLoader加载的jar包路径。这几种类加载器 都遵循双亲委派机制双亲委派机制说的其实就是,当一个类加载器收到一个类加载请求时,会去判断有没有加载过,如果加载过直接返回,否则该类加载器会把请求先委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。双亲委派模式优势:避免类的重复加载, 当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次, 这样保证了每个类只被加载一次。保护程序安全,防止核心API被随意篡改,比如 java核心api中定义类型不会被随意替换我们这里看一个例子:我们新建一个自己的类“String”放在src/java/lang目录下新建StringTest类:结果:start test——-可以看出,程序并没有运行我们自定义的“String”类,而是直接返回了String.class。像String,Integer等类 是JAVA中的核心类,是不允许随意篡改的!ClassLoader是一个抽象类,负责加载类,像ExtClassLoader,AppClassLoader都是由该类派生出来,实现不同的类装载机制。这块的源码太多了,就不贴了我们来看下 它的核心方法loadClass(),传入需要加载的类名,它会帮你加载:loadClass()源码 展示了,一般加载.class文件大致流程:先去缓存中 检查是否已经加载该类,有就直接返回,避免重复加载;没有就下一步遵循 双亲委派机制,来加载.class文件上面两步都失败了,调用findClass()方法,让当前类加载器加载注意:由于ClassLoader类是抽象类,而抽象类是无法通过new创建对象的,所以它最核心的findClass()方法,没有具体实现,只抛了一个异常,而且是protected的,这是应用了模板方法模式,具体的findClass()方法丢给子类实现, 所以继承的子类得重写该方法。那我们仿照ExtClassLoader,AppClassLoader来实现一个自定义的类加载器,我们同样是继承ClassLoader类编写一个测试类TestPerson接着 编写一个自定义类加载器MyTestClassLoader:最后在编写一个测试controller:先找到TestPerson所在的目录, 执行命令:javac TestPerson,生成TestPerson.class这里没有使用idea的build,是因为我们代码的class读取路径 是写死了的,不走默认CLASSPATH
D:ideaProjectssrcmainjavacomzjideaprojectstest2TestPerson.class我们然后用postman调用testClassLoader()测试接口结果:当前类加载器:com.zj.ideaprojects.test2.MyTestClassLoader@1d75e392
hello my name is: xiao ming然后修改TestPerson,将name 改为 “xiao niu”然后在当前目录 重新编译, 执行命令:javac TestPerson,会在当前目录重新生成TestPerson.class 不重启项目,直接用postman 直接调这个测试接口 结果:当前类加载器:com.zj.ideaprojects.test2.MyTestClassLoader@7091bd27
hello my name is: xiao niu这样就实现了“热部署”!!!如果不打破的话,结果 当前类加载器会显示”sun.misc.Launcher$AppClassLoader”,原因是由于idea启动项目的时候会自动帮我们编译,将class放到 CLASSPATH路径下。其实可以把默认路径下的.class删除也行。这里也是为了展示如何打破双亲委派机制,才如此实现的。官方推荐我们自定义类加载器时,遵循双亲委派机制。但是凡事得看实际需求嘛通过上面的例子我们可以看出:1、如果不想打破双亲委派机制,我们自定义类加载器,那么只需要重写findClass方法即可2、如果想打破双亲委派机制,我们自定义类加载器,那么还得重写整个loadClass方法如果你阅读到这里,你会发现双亲委派机制的各种好处,但万物都不是绝对正确的,我们需要一分为二地看待问题。在某些场景下双亲委派制过于局限,所以有时候必须打破双亲委派机制来达到目的。比如 :SPI机制、线程上下文类加载器1.SPI(Service Provider Interface)服务提供接口。它是jdk内置的一种服务发现机制,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是让服务定义与实现分离解耦。SPI机制图2.线程上下文类加载器(context class loader)是可以破坏Java类加载委托机制,使程序可以逆向使用类加载器,使得java类加载体系显得更灵活。Java 应用运行的初始线程的上下文类加载器是应用类加载器,在线程中运行的代码可以通过此类加载器来加载类和资源。Java.lang.Thread中的方法getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。SPI机制在框架的设计上应用广泛,下面举几个常用的例子:平时获取jdbc,我们可以这样:Connection connection =DriverManager.getConnection("jdbc://localhost:3306");我们读DriverManager的源码发现:其实就是查询classPath下,所有META-INF下给定Class名的文件,并将其内容返回,使用迭代器遍历,这里遍历的内部使用Class.forName加载了类。其中有一处非常重要ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);我们看下它的实现:我们可以看出JDBC,DriverManager类和ServiceLoader类都是属于核心库rt.jar的,它们的类加载器是Bootstrap ClassLoader类加载器。而具体的数据库驱动相关功能却是第三方提供的,第三方的类不能被引导类加载器(Bootstrap ClassLoader)加载。所以java.util.ServiceLoader类进行动态装载时,使用了线程的上下文类加载器(ThreadContextClassLoader)让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派机制。Tomcat是web容器,我们把war包放到 tomcat 的webapp目录下,这意味着一个tomcat可以部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。防止出现一个应用中加载的类库会影响另一个应用的情况。如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。Tomcat类加载器种类如果Tomcat本身的依赖和Web应用还需要共享,Common类加载器(CommonClassLoader)来装载实现共享Catalina类加载器(CatalinaClassLoader) 用来 隔绝Web应用程序与Tomcat本身的类Shared类加载器(SharedClassLoader):如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载WebAppClassLoader:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器WebAppClassLoader(多个应用程序,就有多个WebAppClassLoader)负责优先加载本身的目录下的class文件加载不到时再交给CommonClassLoader以及上层的ClassLoader进行加载这破坏了双亲委派机制。Jsp类加载器(JasperLoader):实现热部署的功能,修改文件不用重启就自动重新装载类库。JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。我们来模拟一下tomcat 多个版本代码共存:这边的例子换了个电脑,所以目录结构、路径与上面的例子有点变化我们先编写 App类javac App生成的App.class 放入tomcatTestwar1comzjdemotesttomcatTest目录下将name改为webapp 2,重新生成的App.class放入tomcatTestwar2comzjdemotesttomcatTest目录下然后我们编写类加载器:最后编写测试controller:然后postman 调一下这个接口, 结果:当前类加载器:com.zj.demotest.tomcatTest.MyTomcatClassloader@18fbb876
this is webapp 1
当前类加载器:com.zj.demotest.tomcatTest.MyTomcatClassloader@5f7ed4a9
this is webapp 2我们发现2个同样的类能共存在同一个JVM中,互不影响。注意:同一个JVM内,2个相同的包名和类名的对象是可以共存的,前提是他们的类加载器不一样。所以我们要判断多个类对象是否是同一个,除了要看包名和类名相同,还得注意他们的类加载器是否一致springboot自动配置的原因是因为使用了@EnableAutoConfiguration注解。当程序包含了EnableAutoConfiguration注解,那么就会执行下面的方法,然后会加载所有spring.factories文件,将其内容封装成一个map,spring.factories其实就是一个名字特殊的properties文件。在spring-boot应用启动时,会调用loadFactoryNames方法,其中传递的一个参数就是:org.springframework.boot.autoconfigure.EnableAutoConfigurationMETA-INF/spring.factories会被读取到。它还使用了this.getBeanClassLoader() 获取类加载器。所以我们立刻明白了文章一开始的例子,SpringBoot项目直接build项目,不重启项目,就能实现热部署效果。感谢各位的阅读,以上就是“Java类加载器与双亲委派机制怎么应用”的内容了,经过本文的学习后,相信大家对Java类加载器与双亲委派机制怎么应用这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是百云主机,小编将为大家推送更多相关知识点的文章,欢迎关注!

相关推荐: windows系统下如何搭建Golang开发环境

本文小编为大家详细介绍“windows系统下如何搭建Golang开发环境”,内容详细,步骤清晰,细节处理妥当,希望这篇“windows系统下如何搭建Golang开发环境”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。Go语言(又称Go…

免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。

(0)
打赏 微信扫一扫 微信扫一扫
上一篇 03/29 15:40
下一篇 03/29 15:41

相关推荐