上图是java类的加载过程以及涉及到类加载:
1.我们都知道.java文件终极要编译成.class文件才可以被实行,例如com.tynet.module.admin.controller.LoginController.java文件会被编译成com.tynet.module.admin.controller.LoginController.class这种class文件然后放入JVM才可以被实行。
2.既然说到了JVM肯定有创建JVM的地方,windows系统下java.exe调用底层的jvm.dll文件创建java虚拟机,然后通过C++的实现来创建勾引类加载器,他的父类加载器是null,由于他自己便是最底层的类加载器器了。勾引类加载器:卖力加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等。
3.c++调用java代码创建JVM启动器,实例化sun.misc.Launcher这个工具,该类由勾引类加载器卖力加载和创建,然后在由勾引类加载器分别创建扩展类加载器和运用类加载器,下面是核心的代码,分别在创建的时候设置父类加载器。
扩展类加载器:卖力加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
运用程序类加载器:卖力加载ClassPath路径下的类包,紧张便是加载你自己写的那些类
// 在启动的时候调用静态方法创建了一个launcher工具
private static Launcher launcher = new Launcher();public static Launcher getLauncher() { return launcher; }
扩展类加载器:
勾引类加载器创建扩展类加载器,并且将勾引类加载器作为扩展类加载器父类
Launcher.ExtClassLoader.getExtClassLoader();super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
运用类加载器:
扩展类加载器创建运用类加载器,并且运用类加载器的父类是扩展类加载器
Launcher.ExtClassLoader var1;this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);super(var1, var2, Launcher.factory);Launcher.getClassLoader方法调用类加载器,默认是运用类加载器 public ClassLoader getClassLoader() { return this.loader; } this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
针对不同的类,运行不同的classLoader,来加载不同类信息,比如String这个类是由勾引类加载器加载,jre下面的ext包下的所有jar交给扩展类加载器加载,我们程序自己写的类Controller类等都是运用类加载器加载
4.ClassLoader.loadClass加载要运行的类LoginController,默认利用的是双亲委派的原则。
个中还要经由:加载, 验证,准备, 解析,初始化过程:
加载:java类加载机制是
验证:校验字节码文件的精确性
准备:给类的静态变量分配内存,并授予默认值,比如有一个public static int i = 5,在这个阶段会给i开辟一个空间,并且将i的值设置为0
解析:将符号引用更换为直接引用,该阶段会把符号引用如static public main方法等符号引用更换为在内存中的真实地址,这便是静态链接,还有一种是在类运行的期间才可以知道调用那个类,例如接口的多态,只有在调用期间才可以知道调用那个类,这个属于动态链接。
初始化:对类的静态变量初始化为指定的值,上面的例子这个阶段会将i的值设置为5,初始化的时候实行静态代码块,且只实行一次。
5.实行完上面的步骤就会将该类加入到JVM中运行,当运行结束后,JVM会把该类销毁回收掉。
6.为什么要设计双亲委派机制?
阐明:双亲委派机制说大略点便是,先找父亲加载,弗成再由儿子自己加载
1.沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意修改
举个例子:我们自己定义一个java.lang.String的一个类,在这个类中我们写一个main方法然后实行这个方法
package java.lang;public class String1{ public static void main(String[] args) { System.out.println("打印自己的string类"); }}
打印结果:缺点: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 运用程序类必须扩展javafx.application.Application
结论:
我们自己定义一个String类,根据双亲委派的原则,由于在java的lib下面也有这个类,以是勾引类加载器会加载到这个类,就不会加载自己写的,这样担保了java的源代码无法被修改,防止黑客攻击置入后门程序。
2.避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,担保被加载类的唯一性。
7.如何自定义类加载器:
自定义类加载器只须要继续 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,以是我们自定义类加载器紧张是重写findClass方法。
1.在maven项目中有User这个类,在要加载的路径下也有这个类。
public class MyClassLoaderTest extends ClassLoader{ private String classPath; public MyClassLoaderTest(String classPath) { this.classPath = classPath; } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); //defineClass将一个字节数组转为Class工具,这个字节数组是class文件读取后终极的字节数组。 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } public static void main(String args[]) throws Exception { MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/test"); //D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录 Class clazz = classLoader.loadClass("JVM.User"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); }}
// 实行结果:
自定义类加载器的实行
sun.misc.Launcher$AppClassLoader
// 结论:
按照双亲委派的原则:在项目中自己写的类加载是利用运用类加载器加载,那么只要加载到了就会直接返回结果,就不会在自定义的类加载器中加载这个类,担保的加载类的唯一性。以是实行的结果看出利用的类加载器是运用类加载器。
2.在maven项眼前没有User1这个类,在要加载的目录中有这个类
public class MyClassLoaderTest extends ClassLoader{ private String classPath; public MyClassLoaderTest(String classPath) { this.classPath = classPath; } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); //defineClass将一个字节数组转为Class工具,这个字节数组是class文件读取后终极的字节数组。 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } public static void main(String args[]) throws Exception { MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/test"); Class clazz = classLoader.loadClass("JVM.User1"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); }}
实行结果:
自定义类加载器的实行User1
JVM.MyClassLoaderTest
终极结论:
按照双亲委派的原则:先由父类加载,如果父类无法加载该类,再交给子类加载,现在我们自己写的类,运用类加载器没有加载到,那就交给我们自定义的加载器加载,加载到了直接返回结果,否则抛出非常。从实行结果看我们利用的是自定义的类加载器,打印出要实行的结果。
8.如何冲破双亲委派机制?
阐明:冲破双亲委派机制:便是要加载一个类,交给父类加载器加载,没有找到然后在由子类加载,冲破的话便是说:不交给父类加载,我们自己定义加载的办法和findClass的方法,然后实现加载类。
package JVM.ClassLoad; import java.io.FileInputStream;import java.lang.reflect.Method; / @author: yangwenbin @date: 2021/7/12 11:44 @since: JDK 1.8 @description: 自定义类加载器 冲破双亲委派机制 /public class MyClassLoaderTestPo extends ClassLoader{ private String classPath; public MyClassLoaderTestPo(String classPath) { this.classPath = classPath; } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); //defineClass将一个字节数组转为Class工具,这个字节数组是class文件读取后终极的字节数组。 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } / 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载 @param name @param resolve @return @throws ClassNotFoundException / protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } if (resolve) { resolveClass(c); } return c; } } / @author: yangwenbin @date: 2021/7/12 13:51 @since: JDK 1.8 @description: 冲破双亲委派机制的话 user1类的父类是OBJECT类 他也会加载object类 现在冲破了双亲委派机制 找不到类了 Prohibited package name: java.lang @param: [args] @return: void / public static void main(String args[]) throws Exception { MyClassLoaderTestPo classLoader = new MyClassLoaderTestPo("D:/test"); //考试测验用自己改写类加载机制去加载自己写的java.lang.String.class Class clazz = classLoader.loadClass("JVM.User1"); Object obj = clazz.newInstance(); Method method= clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); // 自定义类加载器的实行User1 //JVM.MyClassLoaderTestPo }}
用上面的办法会产生的结果是:这样会产生一个问题便是:我们都知道一个类的顶层的类是Object类,这样的话我们冲破了双亲委派机制,这样父类加载器勾引类加载器就无法加载Object类,当我们在运行的时候就会提示找不到Object类,java.io.FileNotFoundException: D:\test\java\lang\Object.class (系统找不到指定的路径。)
办理办法:既然java出于安全的考虑,防止Object类被修改,这样像String,Object这样的类只能由勾引类加载器加载,那么我们就定义那个路径下的是由我们的自己定义的类加载器加载,其他的像object还是交给勾引类加载器加载,这样就可以了。
修正一下loadClass方法:(自己写的利用自己的类加载器加载,Object的那种类还是利用双亲委派办法)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { if(name.startsWith("JVM")){ c = findClass(name); }else{ // 其他的有可能便是像OBJECT就须要勾引类加载器加载 还是要利用双亲委派机制 c = this.getParent().loadClass(name); } sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } if (resolve) { resolveClass(c); } return c; } }
实行结果:
自定义类加载器的实行User1
JVM.MyClassLoaderTestPo
这样就实现了冲破双亲委派机制。
9.tomcat如何冲破双亲委派模式?
上图便是tomcat类加载的基本模型:
以Tomcat类加载为例,Tomcat 如果利用默认的双亲委派类加载机制行弗成?
我们思考一下:Tomcat是个web容器, 那么它要办理什么问题:
1. 一个web容器可能须要支配两个运用程序,不同的运用程序可能会依赖同一个第三方类库的不同版本,不能哀求同一个类库在同一个做事器只有一份,因此要担保每个运用程序的类库都是独立的,担保相互隔离。
2. 支配在同一个web容器中相同的类库相同的版本可以共享。否则,如果做事器有10个运用程序,那么要有10份相同的类库加载进虚拟机。
3. web容器也有自己依赖的类库,不能与运用程序的类库稠浊。基于安全考虑,该当让容器的类库和程序的类库隔离开来。
4. web容器要支持jsp的修正,我们知道,jsp 文件终极也是要编译成class文件才能在虚拟机中运行,但程序运行后修正jsp已经是司空见惯的事情, web容器须要支持 jsp 修正后不用重启。
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader利用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以利用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它涌现的目的便是为了被丢弃:当Web容器检测到JSP文件被修正时,会更换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
tomcat 这种类加载机制违背了java 推举的双亲委派模型了吗?答案是:违背了。
很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会通报给父类加载器,冲破了双亲委派机制。
10.仿照实现Tomcat的webappClassLoader加载自己war包运用内不同版本类实现相互共存与隔离?
package JVM.ClassLoad; import java.io.FileInputStream;import java.lang.reflect.Method; / @author: yangwenbin @date: 2021/7/12 16:34 @since: JDK 1.8 @description: tomcat冲破双亲委派机制 /public class MyClassLoaderTomcat extends ClassLoader{ private String classPath; public MyClassLoaderTomcat(String classPath) { this.classPath = classPath; } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } / 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载 @param name @param resolve @return @throws ClassNotFoundException / protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //非自定义的类还是走双亲委派加载 if (!name.startsWith("JVM")){ c = this.getParent().loadClass(name); }else{ c = findClass(name); } // this is the defining class loader; record the stats sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } if (resolve) { resolveClass(c); } return c; } } public static void main(String args[]) throws Exception { MyClassLoaderTomcat classLoader = new MyClassLoaderTomcat("D:/test"); Class clazz = classLoader.loadClass("JVM.User1"); Object obj = clazz.newInstance(); Method method= clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader()); System.out.println(); MyClassLoaderTomcat classLoader1 = new MyClassLoaderTomcat("D:/test1"); Class clazz1 = classLoader1.loadClass("JVM.User1"); Object obj1 = clazz1.newInstance(); Method method1= clazz1.getDeclaredMethod("sout", null); method1.invoke(obj1, null); System.out.println(clazz1.getClassLoader()); }} // 实行结果:自己利用自己类路径下的类名 然后利用自己定义的类加载器加载,也冲破了双亲委派机制,不用父类加载// 自定义类加载器的实行User1// JVM.MyClassLoaderTomcat@6ce253f1//// 自定义类加载器的实行User1// JVM.MyClassLoaderTomcat@65ab7765
感激大家,欢迎示正学习。