当前位置: 首页 > news >正文

网站代理工具安贞做网站公司

网站代理工具,安贞做网站公司,frontpage2003网页制作教程,哪个旅游网站做的最好文章目录 Jvm基本组成一.什么是JVM类的加载二.类的生命周期阶段1#xff1a;加载阶段2#xff1a;验证阶段3#xff1a;准备阶段4#xff1a;解析阶段5#xff1a;初始化 三.类初始化时机四.类加载器1.引导类加载器#xff08;Bootstrap Class Loader#xff09;2.拓展类… 文章目录 Jvm基本组成一.什么是JVM类的加载二.类的生命周期阶段1加载阶段2验证阶段3准备阶段4解析阶段5初始化 三.类初始化时机四.类加载器1.引导类加载器Bootstrap Class Loader2.拓展类加载器Extension Class Loader3.应用程序类加载器System Class Loader4.自定义类加载器Custom Class Loader5.类加载器的关系6.JVM类加载策略 五.双亲委派机制1.什么是双亲委派机制2.浅析ClassLoader类3.双亲委派模式具体实现3.1.loadClass(String name)3.2.findClass(String name)-(重点)3.3.defineClass(byte[] b, int off, int len)3.4.resolveClass(Class≺?≻ c)3.5.URLClassLoader类 4.设计双亲委派机制的目的5.ExtClassLoader和AppClassLoader 六.Tomcat 中的类加载器1.Tomcat类加载器类型说明2.Tomcat为什么要打破双亲委派模型3.线程上下文类加载器 七.如何打破双亲委派模型八.打破双亲委派模型的常见场景九.热部署类加载器 Jvm基本组成 JVM 的结构基本上由 5 部分组成 类加载器ClassLoader在 JVM 启动时或者类运行时将需要的 class文件 解析后生成一个Class对象加载到到 JVM 中执行引擎ExcutionEngine执行引擎的任务是负责执行 class 文件中包含的字节码指令相当于实际机器上的 CPU内存区Memory Area将内存划分成若干个区以模拟实际机器上的存储、记录和调度功能模块如实际机器上的各种功能的寄存器或者 PC 指针的记录器等由Heap、程序计数器、虚拟机栈、本地方法栈和方法区五部分组成。本地方法调用Native Method Interface调用 C 或 C 实现的本地方法的代码返回结果垃圾收集器Garbage Collection负责将内存区中无用对象的释放主要是堆内存和方法区 JVM被分为三个主要的子系统类加载器子系统、运行时数据区、执行引擎 一.什么是JVM类的加载 Java代码编译执行过程 源码编译通过Java源码编译器(Javac命令)将Java代码编译成Jvm字节码.class文件类加载通过ClassLoader及其子类来完成Jvm的类加载类执行字节码被装入内存进入Jvm被解释器(Java命令)解释执行 什么是类的加载 从上图来看其实可以一句话来解释类的加载指的是将类的.class文件中的二进制数据读入到内存中并对class文件中的数据进行校验、转换、解析、初始化等操作后将其放在运行时数据区的 方法区内然后在堆区创建一个 Java.lang.Class对象封装类在方法区内的数据结构。 二.类的生命周期 类从JVM加载到卸载出内整个生命周期包括加载、验证、准备、解析、初始化、使用和卸载 7个阶段 类加载包含了前5个其中 验证、准备、解析为连接。具体如图 阶段1加载 通过一个类的全路径查找此类字节码文件并利用class文件创建对应的Class对象并加载到方法区 阶段2验证 确保Class文件的字节流中包含信息符合虚拟机规范而不会危害虚拟机自身运行安全。主要包括4种验证文件格式验证元数据验证字节码验证符号引用验证。 阶段3准备 为类变量(即static修饰的字段变量)分配内存并且设置该类变量的默认值 (如static int i5; 这里只将i初始化为0至于5的值将在初始化时赋值)这里不包含用final修饰的static变量因为final在编译的时候就会分配了且这里不会为实例变量分配初始化类变量会分配在方法区中而实例变量是会随着对象一起分配到Java堆中。 阶段4解析 解析阶段是将常量池中的符号引用替换为直接引用的过程。 将符号引用替换为直接引用该阶段会把一些静态方法符号引用比如 main() 方法替换为指向数据所存内存的指针或句柄等直接引用这就是所谓的静态链接过程类加载期间完成动态链接是在程序运行期间完成的将符号引用替换为直接引用。主要工作类或接口的解析字段解析类方法解析接口方法解析 例如在com.xxx.Person类中引用了com.xxx.Animal类在编译阶段Person类并不知道Animal的实际内存地址因此只能用com.xxx.Animal来代表Animal真实的内存地址。而在解析阶段JVM可以通过解析该符号引用来确定com.xxx.Animal类的真实内存地址如果该类未被加载过则先加载。 符号引用就是一组符号来描述目标可以是任何字面量而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。 阶段5初始化 初始化阶段是类加载过程的最后一步这一步才 真正开始执行类中定义的Java程序代码。 即对类的静态变量初始化为指定的值执行静态代码块。 在之前的准备阶段类中定义的static静态变量已经被赋过一次默认值。而在初始化阶段则会调用类构造器clinit来完成初始化操作为静态变量赋原始值。 此处需要注意的是类构造器clinit和类构造方法init区别。 类构造方法init每创建一次对象就自动调用一次而类构造器clinit类似于一个无参的构造函数只不过该函数是静态修饰只会初始化静态所修饰的代码。 public class Student{public static int x 10;public String zz jiaboyan;static{System.out.println(12345);} }类构造器clinit是由编译器自动收集类中的所有静态变量的赋值动作和静态语句块static{}中的代码合并产生的编译器收集的顺序是由 语句在源文件中出现的顺序所决定的 综上所述对于上面的例子来说类构造器clinit为 public class Student{clinit{public static int x 10;System.out.println(12345);} }三.类初始化时机 JVM规范中并没有强制约束何时进行加载但是规范严格规定了有且只有下列5种情况必须对类进行初始化加载、验证、准备都会随着发生 遇到 new、getstatic、putstatic、invokestatic 这4条字节码指令时如果类没有进行过初始化则必须先触发其初始化。 最常见的生成这 4 条指令的场景是1.使用 new 关键字实例化对象的时候2.读取或设置类的静态字段被 final 修饰、已在编译器把结果放入常量池的静态字段除外的时候 3.调用一个类的静态方法的时候。 使用 java.lang.reflect 包的方法对类进行反射调用的时候如果类没有进行初始化则需要先触发其初始化。 当初始化一个类的时候如果发现其父类还没有进行过初始化则需要先触发其父类的初始化。 当虚拟机启动时用户需要指定一个要执行的主类(启动类)包含 main() 方法的那个类虚拟机会先初始化这个主类 当使用 JDK.7 的动态语言支持时如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄并且这个方法句柄所对应的类没有进行过初始化则需要先触发其初始化 以上 5 种场景中的行为称为对一个类进行主动引用。除此之外所有引用类的方式都不会触发初始化称为被动引用。被动引用的常见例子包括 通过子类引用父类的静态字段不会导致子类初始化。通过数组定义来引用类不会触发此类的初始化。该过程会对数组类进行初始化数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类其中包含了数组的属性和方法。常量在编译阶段会存入调用类的常量池中本质上并没有直接引用到定义常量的类因此不会触发定义常量的类的初始化。 特殊情况 当 Java 虚拟机初始化一个类时要求它所有的父类都被初始化但这一条规则并不适用于接口。一个父接口并不会因为他的子接口或者实现类的初始化而初始化只有当程序首次被使用特定接口的静态变量时才会导致该接口的初始化。 代码块、静态方法、构造方法加载顺序 public class Son extends Father{static { System.out.println(子类静态代码块); }{ System.out.println(子类代码块); }public Son() { System.out.println(子类构造方法); }public static void main(String[] args) {new Son();}}class Father{static { System.out.println(父类静态代码块); }{System.out.println(父类代码块);}public Father() { System.out.println(父类构造方法);}public static void find() {System.out.println(静态方法);} } //代码块和构造方法执行顺序 //父类静态代码块 //子类静态代码块 //父类代码块 //父类构造方法 //子类代码块 //子类构造方法四.类加载器 类加载器的作用 通过类全限定名读取类的二进制字节流到JVM中然后转换为一个与目标类对应的java.lang.Class对象实例 1.引导类加载器Bootstrap Class Loader 引导类加载器也叫启动类加载器 主要加载的是JVM自身需要的类该类加载使用C实现的是虚拟机自身的一部分负责加载 JAVA_HOME\lib\ 目录下的核心类库或被 -Dbootclaspath 参数指定的类, 如: rt.jar, tool.jar 等 2.拓展类加载器Extension Class Loader 拓展类加载器 由Sun公司通过Java在sun.misc.Launcher$ExtClassLoader类中实现 负责加载 JAVA_HOME\lib\ext 扩展目录中的类库或 -Djava.ext.dirs 选项所指定目录下的类和 jar包开发者可以直接使用标准扩展类加载器, //Launcher$ExtClassLoader类中获取路径的代码 private static File[] getExtDirs() {//加载JAVA_HOME/lib/ext目录中的类库String s System.getProperty(java.ext.dirs);File[] dirs;if (s ! null) {StringTokenizer st new StringTokenizer(s, File.pathSeparator);int count st.countTokens();dirs new File[count];for (int i 0; i count; i) {dirs[i] new File(st.nextToken());}} else {dirs new File[0];}return dirs;} 3.应用程序类加载器System Class Loader 应用程序类加载器也称为系统类加载器由Sun公司通过Java在sun.misc.Launcher$AppClassLoader中实现 负责加载 环境变量 CLASSPATH 或 -Djava.class.path、java -cp所指定的目录下的类和 jar 包。可以通过ClassLoader#getSystemClassLoader()获取到该类加载器开发者可以直接使用系统类加载器该加载器是程序中默认类加载器 public class MainClassLoaderTest {public static void main(String[] args) {System.out.println(String.class.getClassLoader());System.out.println(DESKeyFactory.class.getClassLoader());System.out.println(MainClassLoaderTest.class.getClassLoader());/*nullsun.misc.Launcher$ExtClassLoader246b179dsun.misc.Launcher$AppClassLoader18b4aac2*/System.out.println();ClassLoader appClassLoader ClassLoader.getSystemClassLoader();//应用程序类加载器ClassLoader extClassLoader appClassLoader.getParent();//拓展类加载器ClassLoader bootstrapClassLoader extClassLoader.getParent();//引导类加载器System.out.println(bootstrapClassLoader: bootstrapClassLoader);System.out.println(extClassLoader: extClassLoader);System.out.println(appClassLoader: appClassLoader);/*bootstrapClassLoader: nullextClassLoader: sun.misc.Launcher$ExtClassLoader246b179dappClassLoader: sun.misc.Launcher$AppClassLoader18b4aac2*///BootstrapLoader 父加载器 及 加载路径System.out.println();System.out.println(bootstrapLoader 加载以下文件);URL[] urls Launcher.getBootstrapClassPath().getURLs();for (URL url : urls) {System.out.println(url);}/*bootstrapLoader 加载以下文件file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/resources.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/rt.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/sunrsasign.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jsse.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jce.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/charsets.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jfr.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/classes*///ExtClassLoader 父加载器 及 加载路径System.out.println();System.out.println(extClassLoader 加载以下文件);System.out.println(System.getProperty(java.ext.dirs));/*extClassLoader 加载以下文件E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\ext;C:\Windows\Sun\Java\lib\ext*///AppClassLoader父加载器 及 加载路径System.out.println();System.out.println(appClassLoader 加载以下文件);System.out.println(System.getProperty(java.class.path));/*appClassLoader 加载以下文件E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\charsets.jar;省略.....E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\rt.jar;省略E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\resources.jar;省略E:\04_resource_study\java_base_demo\target\classes;省略E:\dev_tools\mavenLocalStorage\org\springframework\boot\spring-boot-starter-web\2.2.0.RELEASE\spring-boot-starter-web-2.2.0.RELEASE.jar;E:\dev_tools\mavenLocalStorage\com\alibaba\fastjson\1.2.78\fastjson-1.2.78.jar;E:\dev_tools\mavenLocalStorage\mysql\mysql-connector-java\8.0.18\mysql-connector-java-8.0.18.jar;*/} }可看出Launcher采用了单例设计模式其中BootstrapClassLoader 、ExtClassLoader和AppClassLoader的扫描路径分别对应系统属性sun.boot.class.path、java.ext.dirs、java.class.path 4.自定义类加载器Custom Class Loader 自定义类加载器负责加载用户自定义包路径下的类包通过 ClassLoader 的子类实现 Class 的加载。如何实现 继承抽象类ClassLoader重写findClass方法判断当前类的class文件是否已被加载 /*** 自定义类加载器*/ public class MyClassLoader extends ClassLoader {//包路径private String path;//构造方法用于初始化Path属性public MyClassLoader(String path) {this.path path;}//重写findClass方法参数name表示要加载类的全类名(包名.类名)Overrideprotected Class? findClass(String name) throws ClassNotFoundException {//检查该类的class文件是否已被加载如果已加载则返回class文件(字节码文件)对象如果没有加载返回nullClass? loadedClass findLoadedClass(name);//如果已加载直接返回该类的class文件(字节码文件)对象if (loadedClass ! null) {return loadedClass;}//字节数组用于存储class文件的字节流byte[] bytes null;try {//获取class文件的字节流bytes getBytes(name);} catch (Exception e) {e.printStackTrace();}if (bytes null) {throw new ClassNotFoundException();}//如果字节数组不为空则将class文件加载到JVM中//将class文件加载到JVM中返回class文件对象//字节码数组加载到 JVM 的方法区并在 JVM 的堆区建立一个java.lang.Class对象的实例,用来封装 Java 类相关的数据和方法return this.defineClass(name, bytes, 0, bytes.length);}//获取class文件的字节流private byte[] getBytes(String name) throws Exception {//拼接class文件路径 replace(.,File.separator) 表示将全类名中的.替换为当前系统的分隔符File.separator返回当前系统的分隔符String fileUrl path name.replace(., File.separator) .class;//缓冲区byte[] buffer new byte[1024];//输入流InputStream fis new FileInputStream(new File(fileUrl));//相当于一个缓存区动态扩容也就是随着写入字节的增加自动扩容ByteArrayOutputStream baos new ByteArrayOutputStream();//循环将输入流中的所有数据写入到缓存区中int bytesNumRead 0;// 读取类文件的字节码while ((bytesNumRead fis.read(buffer)) ! -1) {baos.write(buffer, 0, bytesNumRead);}baos.flush();baos.close();return baos.toByteArray();}public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {String path E:/04_resource_study/java_base_demo/target/classes/;//创建自定义类加载器对象MyClassLoader classLoader new MyClassLoader(path);System.out.println(MyDamageClassLoader的父加载器: classLoader.getParent());//返回加载的class对象Class? clazz classLoader.findClass(com.demo.classload.ClassLoaderTest);//调用类的构造方法创建对象Object o clazz.newInstance();//输出创建的对象System.out.println(创建的对象:o);//输出当前类加载器System.out.println(ClassLoaderTest当前类加载器:clazz.getClassLoader());//输出当前类加载器的父类System.out.println(ClassLoaderTest当前类加载器的父类:clazz.getClassLoader().getParent());//输出当前类加载器的父类的父类System.out.println(ClassLoaderTest当前类加载器的父类的父类:clazz.getClassLoader().getParent().getParent());//输出当前类加载器的父类的父类的父类System.out.println(ClassLoaderTest当前类加载器的父类的父类的父类:clazz.getClassLoader().getParent().getParent().getParent());/*执行结果:MyDamageClassLoader的父加载器:sun.misc.Launcher$AppClassLoader18b4aac2创建的对象:com.demo.classload.ClassLoaderTest238e0d81ClassLoaderTest当前类加载器:com.demo.classload.MyDamageClassLoader7daf6eccClassLoaderTest当前类加载器的父类:sun.misc.Launcher$AppClassLoader18b4aac2ClassLoaderTest当前类加载器的父类的父类:sun.misc.Launcher$ExtClassLoader31221be2ClassLoaderTest当前类加载器的父类的父类的父类:null*/} } 为什么要自定义类加载器? 当class文件不在ClassPath路径下默认系统类加载器无法找到该class文件当一个class文件是通过网络传输并且可能会进行相应的加密操作时需要先对class文件进行相应的解密后再加载到JVM内存中当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能) 5.类加载器的关系 JVM 的类加载器具有父子关系但不是通过继承来实现而是每个类加载器通过组合方式维护一个 parent字段指向父加载器。启动类加载器(Bootstrap Class Loader)由C实现没有父类。启动类加载器无法被java程序直接引用。拓展类加载器(Extension ClassLoader)由Java语言实现父类加载器为null(如果parentnull则它的父级就是启动类加载器。)系统类加载器(AppClassLoader)由Java语言实现父类加载器为ExtClassLoader自定义类加载器(Custom Class Loader父类加载器肯定为AppClassLoader。 6.JVM类加载策略 按需加载 JVM对Class文件采用的是按需加载的方式也就是说当需要使用该类时才会将它的Class文件加载到内存生成Class对象 全盘负责当一个类加载器负责加载某个Class时该Class所依赖的和引用的其他Class也将由该类加载器负责一起加载除非显示使用另外一个类加载器来载加载 父类委托双亲委派先让父类加载器试图加载该类只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类为了避免不同的加载器 加载相同的类 出现重复字节 缓存机制缓存机制将会保证所有加载过的Class都会被缓存当程序中需要使用某个Class时类加载器先从缓存区寻找该Class只有缓存区不存在系统才会读取该类对应的二进制数据并将其转换成Class对象存入缓存区。这就是为什么修改了Class后必须重启JVM程序的修改才会生效 在JVM中表示两个class对象是否为同一个类对象存在2个必要条件 类的全限定名必须一致(即包名.类名)加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。 即JVM中两个类对象(class对象)就算来源同一个Class文件被同一个虚拟机所加载但只要加载它们的ClassLoader实例对象不同那么这两个类对象也是不相等的这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间所以加载的class对象也会存在不同的类名空间中但前提是重写loadClass() 因为双亲委派模型第一步会通过Class c findLoadedClass(name)从缓存查找类全限定名相同则不会再次被加载因此我们必须跳过缓存查询才能重新加载class对象。当然也可直接调用findClass()方法这样也避免从缓存查找. 五.双亲委派机制 1.什么是双亲委派机制 JVM 的类加载器具有父子关系双亲委派机制是在Java 1.2后引入的其工作原理的是 如果一个类加载器收到了类加载请求它并不会自己先去加载而是把这个请求委托给父类的加载器去执行如果父类加载器还存在其父类加载器则进一步向上委托依次递归请求最终将到达顶层的启动类加载器(Bootstrap Class Loader)如果启动类加载器可以完成类加载任务就成功返回否则就一层一层向下委派子加载器去尝试加载这就是双亲委派模式即每个儿子都很懒每次有活就丢给父亲去干直到父亲说这件事我也干不了时儿子自己想办法去完成这不就是传说中的实力坑爹啊 如图所示 当System ClassLoader拿到一个class 文件之后, 会先问父加载器Extension ClassLoader能不能加载当Extension ClassLoader 接收到请求时,会先问问他的父加载器BootStrap ClassLoader能不能加载如果 Bootstrap ClassLoader可以加载,则由Bootstrap ClassLoader来加载如不能则由Extension ClassLoader来加载. 如果 Extension ClassLoader也加载不了的话,最后由System ClassLoader来加载. 那么采用这种模式有啥用呢? 双亲委派机制保证类加载器自下而上的委派又自上而下的加载保证每一个类在各个类加载器中都是同一个类。 2.浅析ClassLoader类 JVM 并不是在启动时就把所有的.class文件都加载一遍而是程序在运行过程中用到了这个类才去加载。除了启动类加载器外其他所有类加载器都需要继承抽象类ClassLoader这个抽象类中定义了3个关键方法理解清楚它们的作用和关系非常重要。 public abstract class ClassLoader {//委托的父类加载器private final ClassLoader parent;//name为类的全限定名(包名.类名) public Class? loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}//name为类的全限定名(包名.类名) ,resolve如果为true则在生成class对象的同时进行解析相关操作protected Class? loadClass(String name, boolean resolve) throws ClassNotFoundException {//同步控制开启并发加载的情况下锁对类对应的锁对象否则为ClassLoader对象本身synchronized (getClassLoadingLock(name)) {//首先检查类是否已经加载Class? c findLoadedClass(name);//1.如果没有被加载if (c null) {long t0 System.nanoTime();try {//如果有父加载器则先委派给父加载器去加载(注意这里是递归调用)if (parent ! null) {c parent.loadClass(name, false);// 如果父加载器为空则委托启动类加载器去加载 } else {c findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//如果没有从非空父加载器中找到类则抛出类异常ClassNotFoundException }//2.如果父加载器没加载成功调用自己实现的findClass去加载if (c null) {//如果仍然没有找到则调用findClass来查找类。long t1 System.nanoTime();c findClass(name);//这是定义类装入器;记录数据sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}//是否生成class对象的同时进行解析相关操作if (resolve) {resolveClass(c);}return c;}}//name为类的全限定名(包名.类名) protected Class? findClass(String name){//1. 根据传入的类名name到在特定目录下去寻找类文件把.class文件读入内存//......省略具体实现......//2. 调用defineClass将字节数组转成Class对象return defineClass(buf, off, len)}//将字节码数组解析成一个Class对象用native方法实现//b:class文件字节数组,off:开始读取位置,len:每次字节数protected final Class? defineClass(byte[] b, int off, int len){//......省略具体实现......}//获取类名对应的锁对象//ClassLoader并发加载是通过一个ConcurrentHashMapString,Object实现的Key为类名对应的Value为一个new Object()//所以它可以同时加载多个类但同一个类重复加载时则可以锁住。通过registerAsParallelCapable()可以启用并发加载。//详见https://blog.csdn.net/w1673492580/article/details/81912344protected Object getClassLoadingLock(String className) {Object lock this;if (parallelLockMap ! null) {Object newLock new Object();lock parallelLockMap.putIfAbsent(className, newLock);if (lock null) {lock newLock;}}return lock;} }从上面的代码可以得到几个关键信息 JVM 的类加载器具有父子关系但不是通过继承来实现而是每个类加载器通过组合方式维护一个 parent字段指向父加载器。 AppClassLoader的parent是ExtClassLoaderExtClassLoader的parent是BootstrapClassLoader但是ExtClassLoader的parentnull defineClass()通过调用 native 方法把 Java 类的字节码解析成一个 Class 对象。findClass()用于找到.class文件并把.class文件读到内存得到字节码数组然后在方法内部调用 defineClass方法得到 Class 对象 子类必须实现findClass。 loadClass()主要职责就是实现双亲委派机制首先检查类是不是被加载过如果加载过直接返回否则委派给父加载器加载这是一个递归调用一层一层向上委派最顶层的类加载器启动类加载器无法加载该类时再一层一层向下委派给子类加载器加载。 如图所示 3.双亲委派模式具体实现 可以看出顶级类加载器是抽象类ClassLoader类除启动类加载器外所有的类加载器都继承自ClassLoader这里主要介绍ClassLoader中几个比较重要的方法。 3.1.loadClass(String name) 该方法就是双亲委派机制的具体实现用于加载指定全限定名的类由ClassLoader类实现在JDK1.2之后不建议开发者重写但可以直接通过this.getClass().getClassLoder.loadClass(className) 调用源码如下 业务逻辑当类加载请求到来时先从缓存中查找该类对象如果存在直接返回如果不存在则交给该类加载器去的父加载器去加载倘若没有父加载器则交给顶级启动类加载器去加载最后倘若仍没有找到则使用findClass()方法去加载 //name为类的全限定名(包名.类名) public Class? loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}//name为类的全限定名(包名.类名) ,resolve如果为true则在生成class对象的同时进行解析相关操作protected Class? loadClass(String name, boolean resolve) throws ClassNotFoundException {//同步锁synchronized (getClassLoadingLock(name)) {//首先检查类是否已经加载Class? c findLoadedClass(name);//1.如果没有被加载if (c null) {long t0 System.nanoTime();try {//如果有父加载器则先委派给父加载器去加载(注意这里是递归调用)if (parent ! null) {c parent.loadClass(name, false);// 如果父加载器为空则委托启动类加载器去加载 } else {c findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//如果没有从非空父加载器中找到类则抛出类异常ClassNotFoundException }//2.如果父加载器没加载成功调用自己实现的findClass去加载if (c null) {//如果仍然没有找到则调用findClass来查找类。long t1 System.nanoTime();c findClass(name);//这是定义类装入器;记录数据sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}//是否生成class对象的同时进行解析相关操作if (resolve) {resolveClass(c);}return c;}} 3.2.findClass(String name)-(重点) 该方法在JDK1.2之后已不建议用户去覆盖loadClass()方法而是建议把自定义的类加载逻辑写在findClass()方法中 findClass()在loadClass()中被调用的在loadClass()中当父加载器加载失败后则会调用自己的findClass()方法来完成类加载这样就可以保证自定义的类加载器也符合双亲委托模式。ClassLoader类没有实现findClass()方法只是简单的抛出ClassNotFoundExceptionprotected Class? findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);//直接抛出异常 }在findClass()中把class文件读到内存得到字节码数组后需要在该方法内部调用 defineClass()方法得到 Class 对象 3.3.defineClass(byte[] b, int off, int len) 该方法由ClassLoader类中实现用于将byte字节流解析成JVM能够识别的Class对象()通常与findClass()方法一起使用 一般情况下在自定义类加载器时会直接覆盖ClassLoader的findClass()方法并重写类加载规则取得要加载类的字节码后转换成流然后调用defineClass()方法生成类的Class对象直接调用defineClass()生成类的Class对象该Class对象并没有解析(也可以理解为链接阶段毕竟解析是链接的最后一步)其解析操作需要等待初始化阶段进行。 代码示例 protected Class? findClass(String name) throws ClassNotFoundException {// 获取类的字节数组byte[] classData getClassData(name); if (classData null) {throw new ClassNotFoundException();} else {//使用defineClass生成class对象return defineClass(name, classData, 0, classData.length);}} 3.4.resolveClass(Class≺?≻ c) 使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。 3.5.URLClassLoader类 URLClassLoader是ClassLoade的具体实现并新增URLClassPath类辅助获取Class字节码流等功能如果自定义类加载器时没有太复杂的需求可以直接继承URLClassLoader类这样就可以避免自己去编写findClass()方法及其获取字节码流的方式使自定义类加载器编写更加简洁 4.设计双亲委派机制的目的 避免类的重复加载当父类加载器已经加载类后子类加载器没必要再次加载保证了类加载的唯一性。 双亲委派保证类加载器自下而上的委派又自上而下的加载保证每一个类在各个类加载器中都是同一个类。 保证 Java 核心库的类型安全Java 核心库中的类加载工作都是由启动类加载器统一来完成的。从而确保了Java 应用所使用的都是同一个版本的 Java 核心类库他们之间是相互兼容的。 如java.lang.Object存在于rt.jar中无论哪个类加载器要加载这个类最终都是委派给启动类加载器加载因此Object类在程序的各种类加载器环境中都是同一个类。 不同的类加载器可以为相同类binary name的类创建额外的命名空间。相同名称的类可以并存在JVM中只需要不同的类加载器来加载他们即可不同的类加载器的类之间是不兼容的这相当于在JVM内部创建了一个又一个相互隔离的Java类空间这类技术在很多框架中得到了实际运用。 5.ExtClassLoader和AppClassLoader 拓展类加载器ExtClassLoader和系统类加载器AppClassLoader都继承自URLClassLoader是sun.misc.Launcher的静态内部类。 sun.misc.Launcher主要被系统用于启动主应用程序 ExtClassLoader并没有重写loadClass()方法这说明其遵循双亲委派模式而AppClassLoader重载了loadCass()方法但最终调用的还是父类loadClass()方法因此依然遵守双亲委派模式重载方法源码如下 /*** Override loadClass 方法新增包权限检测功能*/public Class loadClass(String name, boolean resolve) throws ClassNotFoundException{int i name.lastIndexOf(.);if (i ! -1) {SecurityManager sm System.getSecurityManager();if (sm ! null) {sm.checkPackageAccess(name.substring(0, i));}}//依然调用父类的方法return (super.loadClass(name, resolve));} 其实无论是ExtClassLoader还是AppClassLoader都继承URLClassLoader类因此它们都遵守双亲委托模型 六.Tomcat 中的类加载器 1.Tomcat类加载器类型说明 在Java类加载器基础上Tomcat新增了几个类加载器包括3个基础类加载器和每个Web应用的私有类加载器其中3个基础类加载器可在conf/catalina.properties中配置具体介绍下 Common Class Loader以System Class Loader 为父类是tomcat顶层的公用类加载器其路径由conf/catalina.properties中的common.loader指定默认加载$CATALINE_HOME/lib下的包。 Catalina Class Loader以Common Class Loader 为父类是用于加载Tomcat应用服务器的类加载器其路径由server.loader指定默认为空此时tomcat使用Common Class Loader加载应用服务器。 Shared ClassLoader以Common Class Loader 为父类是所有Web应用的父加载器其路径由shared.loader指定默认为空此时Tomcat使用Common Class Loader类加载器作为Web应用的父加载器。 Webapp ClassLoader: 以Shared ClassLoader为父类加载/WEB-INF/classes目录下的未压缩的Class 和 资源文件以及 /WEB-INF/lib目录下的jar包是各个 Webapp 私有的类加载器 加载路径中的 class 只对当前 webapp 可见 好处每个的Context(web应用/war包)都使用独立的ClassLoader来加载web应用中的WEB-INF/libs目录下的jar包 因此不同的web应用包不会冲突。如A应用用的是spring 4.X , B应用用的是spring 5.X , 他们可以在同一个tomcat中运行 默认情况下Common、Catalina、Shared类加载器是同一个但可以配置3个不同的类加载器使他们各司其职。 2.Tomcat为什么要打破双亲委派模型 我们都知道一个Tomcat 是可以部署多个 war 包的如果部署的多个war包中依赖的Jar包是不同版本的比如War包A依赖 Spring 4War包B依赖 Spring5 ,这时根据双亲委派机制Spring4首先被加载进来那么另一个依赖 Spring 5 的 War包B在加载时就不会再去加载 Spring 5 。因为同名的原因会直接给他返回已加载过的 Spring 4 。这时会出现版本不一致的问题。因此对于 Tomcat 来说他就需要 自己实现类加载器来打破双亲委派模型并给每一个war包去生成一个自己对应的类加载器 如何打破? Tomcat在初始化的时候通过Thread.currentThread().setContextClassLoader(xx)设置成了Catalina ClassLoader使用Catalina ClassLoader来加载Tomcat使用的类当Tomcat加载WebApp中的类时设置成了WebappClassLoader而WebappClassLoader重写了loadClass方法打破了双亲委派。 Web应用默认的类加载顺序是打破了双亲委派规则 先从JVM的BootStrapClassLoader中加载。加载Web应用下/WEB-INF/classes中的类。加载Web应用下/WEB-INF/lib/中的jar包中的类。加载上面定义的System路径下面的类。加载上面定义的Common路径下面的类。 如果在配置文件中配置了Loader delegatetrue/那么就是遵循双亲委派规则加载顺序如下 先从JVM的BootStrapClassLoader中加载。加载上面定义的System路径下面的类。加载上面定义的Common路径下面的类。加载Web应用下/WEB-INF/classes中的类。 加载Web应用下/WEB-INF/lib/中的jar包中的类。 3.线程上下文类加载器 线程上下文类加载器ContextClassLoader是JDK 1.2开始引入一种类加载器传递机制。可以通过Thread.currentThread().setContextClassLoader(xx)方法给一个线程设置上下文类加载器在该线程后续执行过程中就能通过Thread.currentThread().getContextClassLoader()获取该类加载器使用。 该加载器的出现就是为了方便破坏双亲委派如果没有手动设置上下文类加载器线程将继承其父线程的上下文类加载器默认线程的上下文类加载器是系统类加载器AppClassLoader, 在线程中运行的代码可以通过该类加载器来加载类和资源 七.如何打破双亲委派模型 双亲委派模型是Java设计者推荐给开发者们的默认类加载器实现方式。这个委派和加载顺序是非强制性可破坏的。主要有2种方式 自定义类加载器继承ClassLoader重写findClass()然后再重写loadClass()改变双亲委派的类加载顺序。 通过线程上下文类加载器的传递性让父类加载器中调用子类加载器的加载动作。 下面使用方式1基于代码 4.自定义类加载器Custom Class Loader重写loadClass()即可 public class MyClassLoader extends ClassLoader {//......................省略其他代码......................//重点 重写loadClass()改变双亲委派的类加载顺序Overridepublic Class? loadClass(String name) throws ClassNotFoundException{//1.破坏双亲委派的位置自定义类加载机制先委派给ExtClassLoader加载ExtClassLoader再委派给BootstrapClassLoader如果都加载不了然后自定义类加载器加载自定义类加载器加载不了才交给SystemClassLoaderClassLoader classLoader getSystemClassLoader();while (classLoader.getParent() ! null) {classLoader classLoader.getParent();}Class? clazz null;try {clazz classLoader.loadClass(name);} catch (ClassNotFoundException e) {// Ignore}if (clazz ! null) {return clazz;}//2.自己加载clazz this.findClass(name);if (clazz ! null) {return clazz;}// 3.自己加载不了再调用父类loadClass保持双亲委派模式return super.loadClass(name);}//......................省略其他代码......................public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {String path E:/04_resource_study/java_base_demo/target/classes/;//创建自定义类加载器对象MyClassLoader classLoader new MyClassLoader(path);System.out.println(MyDamageClassLoader的父加载器: classLoader.getParent());//返回加载的class对象Class? clazz classLoader.findClass(com.demo.classload.ClassLoaderTest);//调用类的构造方法创建对象Object o clazz.newInstance();//输出创建的对象System.out.println(创建的对象:o);//输出当前类加载器System.out.println(ClassLoaderTest当前类加载器:clazz.getClassLoader());//输出当前类加载器的父类System.out.println(ClassLoaderTest当前类加载器的父类:clazz.getClassLoader().getParent());//输出当前类加载器的父类的父类System.out.println(ClassLoaderTest当前类加载器的父类的父类:clazz.getClassLoader().getParent().getParent());//输出当前类加载器的父类的父类的父类System.out.println(ClassLoaderTest当前类加载器的父类的父类的父类:clazz.getClassLoader().getParent().getParent().getParent());/*执行结果:MyDamageClassLoader的父加载器:sun.misc.Launcher$AppClassLoader18b4aac2创建的对象:com.demo.classload.ClassLoaderTest2e5d6d97ClassLoaderTest当前类加载器:com.demo.classload.MyDamageClassLoader685f4c2eClassLoaderTest当前类加载器的父类:sun.misc.Launcher$AppClassLoader18b4aac2ClassLoaderTest当前类加载器的父类的父类:sun.misc.Launcher$ExtClassLoader238e0d81ClassLoaderTest当前类加载器的父类的父类的父类:nullnull*/} }重写loadClass()后, 类加载时先委派给ExtClassLoader加载ExtClassLoader再委派给BootstrapClassLoader如果都加载不了然后自定义类加载器加载自定义类加载器加载不了才交给SystemClassLoader。 为什么不能直接让自定义类加载器加载呢 不能双亲委派的破坏只能发生在SystemClassLoader及其以下的加载委派顺序ExtClassLoader上面的双亲委派是不能破坏的 因为任何类都是继承自顶级类java.lang.Object而加载一个类时也会加载继承的类如果该类中还引用了其他类则按需加载且类加载器都是加载当前类的类加载器。 如ClassLoaderTest类只隐式继承了Object自定义MyDamageClassLoader 加载了ClassLoaderTest也会加载Object。 如果loadClass()直接调用MyDamageClassLoader的findClass()会报错java.lang.SecurityException: Prohibited package name: java.lang 八.打破双亲委派模型的常见场景 目前比较常见的场景主要有 线程上下文类加载器(Thread.currentThread().setContextClassLoader(xx)、Thread.currentThread().getContextClassLoader())如JDBC 使用线程上下文类加载器加载 Driver 实现类 Tomcat 的多 Web 应用程序 OSGI 实现模块化热部署 九.热部署类加载器 热部署类加载器: 即利用同一个class文件不同的类加载器在内存创建出两个不同的class对象 JVM在加载类之前会检测请求类是否已加载过(即在loadClass()方法中调用findLoadedClass()方法)如果被加载过则直接从 缓存 获取不会重新加载。 前面的自定义类加载器MyClassLoader通过直接调用findClass()方法已具备这个热加载功能 为什么要调用findClass()来实现热加载而不是loadClass()呢? ClassLoader中默认实现的loadClass()方法中调用findLoadedClass()方法进行了检测是否已被加载因此直接调用findClass()方法就可以绕过重用class缓存问题 当然也可以重写loadClass()方法但强烈不建议这么干 注意 同一个class文件最多只能被同一个类加载器的实例调用findClass()加载一次多次加载将报错, 因此必须让同一类加载器的不同实例加载同一个class文件以实现所谓的热部署。 public class FileClassLoader extends ClassLoader {/*** 根路径*/private String rootDir;public FileClassLoader(String rootDir) {this.rootDir rootDir;}/*** 重写findClass逻辑** param className* return* throws ClassNotFoundException*/Overrideprotected Class? findClass(String className) throws ClassNotFoundException {//获取类的class字节数组用于存储class文件的字节流byte[] bytes null;try {//获取class文件的字节流bytes getBytes(className);} catch (Exception e) {e.printStackTrace();}//如果文件为空if (bytes null) {throw new ClassNotFoundException();}//直接生成class对象return defineClass(className, bytes, 0, bytes.length);}//获取class文件的字节流private byte[] getBytes(String className) throws IOException {// 读取类文件的字节String path classNameToPath(className);//缓冲区byte[] buffer new byte[1024];//输入流InputStream fis new FileInputStream(new File(path));//相当于一个缓存区动态扩容也就是随着写入字节的增加自动扩容ByteArrayOutputStream baos new ByteArrayOutputStream();//循环将输入流中的所有数据写入到缓存区中int bytesNumRead 0;// 读取类文件的字节码while ((bytesNumRead fis.read(buffer)) ! -1) {baos.write(buffer, 0, bytesNumRead);}baos.flush();baos.close();return baos.toByteArray();}/*** 类文件的完全路径* param className* return*/private String classNameToPath(String className) {//拼接class文件路径 replace(.,File.separator) 表示将全类名中的.替换为当前系统的分隔符File.separator返回当前系统的分隔符return rootDir File.separatorChar className.replace(., File.separatorChar) .class;} }测试方法 public static void main(String[] args) {//类所在包的绝对路径String rootDir E:/04_resource_study/java_base_demo/target/classes/;//类的全限定名String classPath com.demo.classload.ClassLoaderTest;//创建自定义文件类加载器FileClassLoader loaderA new FileClassLoader(rootDir);FileClassLoader loaderB new FileClassLoader(rootDir);try {//加载指定的class文件,调用loadClass()Class? objectL1 loaderA.loadClass(classPath);Class? objectL2 loaderB.loadClass(classPath);System.out.println(loadClass-object1: objectL1.hashCode());System.out.println(loadClass-object2: objectL2.hashCode());Class? object3 loaderA.loadClass(classPath);Class? object4 loaderB.loadClass(classPath);System.out.println(loadClass-obj3: object3.hashCode());System.out.println(loadClass-obj4: object4.hashCode());Class? object5 loaderA.findClass(classPath);Class? object6 loaderB.findClass(classPath);System.out.println(findClass-object5: object5.hashCode());System.out.println(findClass-object6: object6.hashCode());//执行到这里会报错: java.lang.LinkageError: loader (instance of com/demo/classload/FileClassLoader): attempted duplicate class definition for name: com/demo/classload/ClassLoaderTest//原因 同一个class文件最多只能被同一个类加载器的实例调用findClass()加载一次多次加载将报错, 因此必须让同一类加载器的不同实例加载同一个class文件以实现所谓的热部署。// Class? object7 loaderA.findClass(classPath);// Class? object8 loaderB.findClass(classPath);FileClassLoader loaderC new FileClassLoader(rootDir);FileClassLoader loaderD new FileClassLoader(rootDir);Class? object7 loaderC.findClass(classPath);Class? object8 loaderD.findClass(classPath);System.out.println(findClass-object7: object7.hashCode());System.out.println(findClass-object8: object8.hashCode());} catch (Exception e) {e.printStackTrace();}// 执行结果// loadClass-object1:1751075886// loadClass-object2:1751075886// loadClass-obj3:1751075886// loadClass-obj4:1751075886// findClass-object5:930990596// findClass-object6:1921595561// findClass-object7:87285178// findClass-object8:610998173}结论 同一个类加载器不同实例调用loadClass()加载class每次返回的都是同一个class实例同一个类加载器实例调用findClass()加载class第2次调用会抛出异常 java.lang.LinkageError, 因此每次热部署都要new一个新类加载器实例来调用findClass()加载class以返回不同的class实例 热部署加载监控线程 如何测试 热部署监控线程启动时只需要我们调整type()方法中println打印内容然后将重新编译后的Pay.class文件放到指定类加载路径中就行了 package com.demo.classload; public class Pay {public void type() {System.out.println(微信支付);} }Slf4j public class HotDeploymentTest {public static void main(String[] args) throws InterruptedException {//记录文件上次修改时间AtomicReferenceLong lastModified new AtomicReference(0L);new Thread(() - {while (true) {//类文件所在包根路径String rootDir E:/04_resource_study/java_base_demo/target/classes/;//类文件所在包绝对路径String classAbsolutePath rootDir /com/demo/classload/Pay.class;//类的全限定名String classSourcePath com.demo.classload.Pay;try {File file new File(classAbsolutePath);//文件不存在,线程休眠if (!file.exists()) {int sleep 2000;log.info(文件{}不存在,休眠{}ms, file.getAbsolutePath(), sleep);TimeUnit.MILLISECONDS.sleep(sleep);continue;}//获取文件的上次修改时间Long modified file.lastModified();//如果文件上次修改时间发生改变使用类加载器重载文件if (modified lastModified.get()) {//当前文件的文件上次修改时间lastModified.set(modified);log.info(文件{}存在且发生改变,lastModified:{},modified:{},开始热部署, file.getAbsolutePath(), lastModified.get(), modified);//创建自定义文件类加载器FileClassLoader loader new FileClassLoader(rootDir);//加载指定的class文件,直接调用findClass(),绕过检测机制创建不同class对象。Class? clazz loader.findClass(classSourcePath);//调用类的构造方法创建对象Object obj clazz.newInstance();//获取该对象的方法String methodName type;Method method clazz.getMethod(methodName, null);//执行方法method.invoke(obj, null);}//没有改变线程休眠else {log.info(文件{}存在未发生改变,lastModified:{},modified:{}, file.getAbsolutePath(), lastModified.get(), modified);TimeUnit.MILLISECONDS.sleep(5000);}} catch (Exception e) {e.printStackTrace();}}}).start();} }执行结果
http://www.tj-hxxt.cn/news/231637.html

相关文章:

  • 做铁合金用哪个外贸网站好赣州网站建设
  • 环保厅网站建设的必要性新的网站建设技术方案
  • 信阳网站建设汉狮报价设计师资格证
  • 网站建设营销外包公司排名深圳的网站建设公司那家好
  • dw做的网站怎么做后台广东新闻联播2011
  • WordPress代码改silder池州网站seo
  • 太仓市住房和城乡建设局规网站网站开发实训基本要求
  • 郑州区块链数字钱包网站开发公司网站如何上传数据库
  • 免费网站建设可信吗wordpress 软件站主题
  • 怎样进网站空间模板型网站建设
  • 简述在线推广网站的方法宝安中心做网站
  • 关于文案的网站直邮网站的推广活动怎么做
  • 红酒手机网站模板杭州网站公司
  • 网站设计与网页制作培训自己做盗版影视网站
  • 怎么做公司的中英文网站专门做加盟的网站
  • 开发制作一个网站容桂品牌网站建设
  • 机械外贸网站重庆建站培训
  • 艺术品展览公司网站建设深圳建设局官方网站
  • 有专门做牙膏的网站吗什么专业学网站建设
  • 建设网站的方案浙江省建设安监站网站
  • 公司有必要建设网站吗旅游网站规划建设方案
  • 网站源码酒类三门峡网站优化
  • 网站引流推广怎么做广东省阳江网络问政平台
  • 帮助做职业规划的网站wordpress 360 google
  • php能自己做网站吗帝国cms影视网站模板
  • 如何让新网站快速收录自己的网站怎么做关键词优化
  • 浏阳网站建设hnktwl荆门网站建设公司
  • 深圳做网站专业公司国外做外贸的小网站
  • 北京定制网站开发公司浩森宇特贡井区建设局网站
  • 模板网站建设+百度建一个个人网站要多少钱