不仅是为了面试,还为了从根本上学习和理解Java代码的执行过程,提高自己对Java的理解
添加idea属性打印加载的类 -XX:+TraceClassLoading
在Java代码中,类的加载、连接和初始化都是在运行时完后的,每一个类都通过类加载器加入加载到JVM中(堆中),形成一个虚拟机可以直接使用的Java类型
可以从磁盘、jar、war、网络、自己编写的class文件中加载class文件
分为验证、准备、解析三个阶段
确保类加载的正确性,保证Class文件的字节流不会影响虚拟机的安全(因为class文件可以从任何途径生成),验证失败抛出 VerifyError
,验证通过就把内存中的二进制流存放到JVM的运行时数据区的方法区中
文件开头魔数代表JDK版本号等信息;常量池中是否有不支持的常量
只有验证通过,二进制字节流才会进入内存的方法区存储
验证该类是否有父类,父类是否继承了不允许继承的类(final类);是否实现了父类或者接口中要求实现的方法;类中方法字段是否与父类或者接口匹配(参数类型、返回值类型)
对类的方法体进行验证,保证类型转换是安全的。
通过字节码验证也不一定是安全的, Halting Problem
,没有任何一个程序可以校验所有程序的合法性(比如while true是无法校验的)
发生在符号引用转换为直接引用的时候
确保该符号引用可以找到对应类。
为类的静态变量分配内存(内存中方法区),并将其初始化为默认值(不是自己设置的值,例如 int a=1;
将a赋值为0)
为静态变量赋初始值,执行static块
以下情况将触发初始化:
new
, getstatic
, putstatic
, invokestatic
指令时,如果没有初始化将进行初始化 reflect
包中,将初始化调用类 public class Test8 { public static void main(String[] args) { System.out.println(Son2.s); } } class Father2{ public static int s = 1; static{ System.out.println("hello i am father"); } } class Son2 extends Father2{ //不会打印这句 没有对Son2的主动使用 static { System.out.println("hello i am son"); } } 复制代码
在初始化一个类时,要求其父类已经被初始化
在初始化一个接口时,不要求其父接口被初始化
在初始化一个类时,不要求其实现接口被初始化
接口变量不需要使用public static final修饰 默认是常量
案例:加载静态变量和常量
public class Test1 { public static void main(String[] args) { System.out.println(MyChild.s); } } class MyParent{ /** * 当s申明为static时 会加载父类和子类,但是只会调用父类的static块 * 当s加上final时,表示常量,不会加载任何一个类,编译阶段被放入该Test1类的常量池中 */ public static final String s = "dx"; static { System.out.println("hello i am my parent"); } } class MyChild extends MyParent{ static { System.out.println("i am my child"); } }复制代码
案例:接口初始化
/** * 接口初始化时,不要求父接口被初始化完成 * 常量如果编译时确定,就不会去加载 * 如果时运行时才可以确定的常量,需要加载 */ public class Test4 { public static void main(String[] args) { System.out.println(MyInterfaceSon.b); } } //一直不加载 interface MyInterface{ public static final int a = 5; } interface MyInterfaceSon extends MyInterface{ //会加载,运行时确定 public static final int b = new Random().nextInt(10); //不会加载,编译时就已经确定 //public static final int b = 10; }复制代码
案例:对象数组不被加载
public class Test3 { public static void main(String[] args) { /* * 不会加载MyParen4,数组类型不会导致加载,只会创建数组引用分配空间 */ MyParent3[] myParent = new MyParent3[10]; //class [Ltop.dzou.jvm.MyParent3; //数组类型标志 [L 全限定名 System.out.println(myParent.getClass()); } } class MyParent3{ static{ System.out.println("i am my parent3"); } }复制代码
案例:静态常量的初始化
public class Test5 { public static void main(String[] args) { /** * 调用了getInstance方法 主动进行加载Singleton类 * 准备阶段:初始化count1为0 singleton为null count2为0 * 初始化完成后,按照顺序调用,执行了invokespecial执行了构造函数,执行完count1=1 count2=1 * 调用完后执行了自己的putstatic指令 把count2设置为0 * 最终结果:count1=0 count2=0 */ Singleton singleton = Singleton.getInstance(); System.out.println(singleton.count1); System.out.println(singleton.count2); } } class Singleton{ public static int count1; private static Singleton singleton = new Singleton(); private Singleton(){ count1++;count2++; System.out.println(count1); System.out.println(count2); } public static int count2 = 0; public static Singleton getInstance(){ return singleton; } }复制代码
包含关系:
子加载器包含一个父亲加载器的引用,即使两个加载器属于一种类型的加载器(例如:同一种自定义加载器)
利用的是ClassLoader中构造方法可以传入一个parent也就是指向父类的类加载器的引用,加载时会优先委托给父类
是否可以自定义一个java.lang.System类?
答:不行,因为自定义System在加载时会被委托到启动器类加载器加载,根据全限定名找到真正的System类加载后在执行main函数时会报找不到main方法,原因是自定义的System类不会被加载
public class System { public static void main(String[] args) { } } output: 错误: 在类 java.lang.System 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application复制代码
无法相互兼容
使用(命名空间不同), 确保核心类被优先加载
。 JVM虚拟机类加载器: 启动器加载器
、 扩展类加载器
、 系统加载器
defineClass
完成的,根据Java Doc Converts an array of bytes into an instance of class Class. Before the Class can be used it must be resolved.复制代码
它将一个二进制流转换为一个java.lang.Class对象返回
自己 同一个命名空间内的类是相互可见 有可能
扩展类加载器加载的class文件需要打成jar包
更改系统类加载器目录:修改java.system.class.loader为自定义
命令: java -Djava.system.class.loader /自定义加载器class文件路径
loadClass(String name) |
加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findClass(String name) |
查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findLoadedClass(String name) |
查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。 |
defineClass(String name, byte[] b, int off, int len) |
把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。 |
resolveClass(Class> c) |
链接指定的 Java 类。 |
{% qnimg jvm/4.png %}
案例:反射不导致类的初始化
public class Test9 { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); //classloader不会导致类的初始化 Class<?> c = classLoader.loadClass("top.dzou.jvm.class_load.D"); System.out.println("---------"); //使用反射加载类会导致类的主动使用,从而初始化该类 Class.forName("top.dzou.jvm.class_load.D"); System.out.println(c);; } } class D{ static { System.out.println("hello i am d"); } }复制代码
案例:实现一个类加载器
public class Test10 extends ClassLoader{ private String fileExt = ".class"; private String path = null; public void setPath(String path) { this.path = path; } public Test10(){ super();//super方法会使用系统加载器作为默认类加载器 } @Override protected Class<?> findClass(String s) throws ClassNotFoundException { byte[] data = loadClassData(s); //找到class调用核心defineClass方法返回一个Class对象 return defineClass(s,data,0,data.length); } //自己实现的加载类方法,把文件读取到二进制流中返回 public byte[] loadClassData(String fileName){ InputStream in = null; ByteArrayOutputStream baos = null; byte[] data = null; try { fileName = fileName.replace(".","/"); in = new FileInputStream(new File(path+fileName+this.fileExt)); baos = new ByteArrayOutputStream(); int c = 0; while((c=in.read())!=-1){ baos.write(c); } data = baos.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { try { in.close(); baos.close(); } catch (IOException e) { e.printStackTrace(); } } return data; } public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException { Test10 loader = new Test10(); //调用ClassLoader的loadClass方法 loader.setPath("/home/dzou/java/jvm-learning/target/classes/"); Class<?> c = loader.loadClass("top.dzou.jvm.class_load.Test9"); System.out.println("class:"+c); Object o = c.newInstance(); System.out.println(o); System.out.println(o.getClass().getClassLoader()); } }复制代码
注意:根据双亲委托机制,会先交给父类去加载,也就是系统类加载器加载,系统类加载器能加载成功的话,就不会使用我们自定义的类加载器,所以我们需要把target中的.class文件删除,使用我们自定义的.class文件路径才会让系统类加载器加载失败,从而使用我们自定义的类加载器
public class Test13 { public static void main(String[] args) throws Exception { Test10 loader1 = new Test10(); Test10 loader2 = new Test10(); loader1.setPath("/home/dzou/Downloads/j/classes/"); loader2.setPath("/home/dzou/Downloads/a/"); Class<?> clazz2 = loader2.loadClass("top.dzou.jvm.class_load.Test1"); Class<?> clazz1 = loader1.loadClass("top.dzou.jvm.class_load.Test1"); Object o1 = clazz1.newInstance(); Object o2 = clazz2.newInstance(); System.out.println(o1.getClass().getClassLoader()); System.out.println(o2.getClass().getClassLoader()); System.out.println(o1==o2); } } 输出: top.dzou.jvm.class_load.Test10@6f94fa3e top.dzou.jvm.class_load.Test10@1d44bcfa false复制代码
Launcher
系统和扩展类加载类-> ExtClassLoader/AppClassLoader
内部类-> URLClassLoader
支持通过路径和jar包加载-> SecureClassLoader
支持提供保护permissions权限(具体没有了解)-> ClassLoader
任意两个加载器都可以通过构造方法创建父子关系,即使是同一个类的类加载器
ContextClassLoader就是为了破坏Java双亲委派模型
我们了解了类加载器,现在看一下一个核心的加载器,就是 上下文类加载器ContextClassLoader
我们可以通过 Thread.currentThread().getContextClassLoader()
获取当前上下文类加载器
通过 Thread.currentThread().setContextClassLoader(ClassLoader cl);
来设置上下文类加载器
依赖规则:我们知道每一个类都会使用自己的类加载器加载该类中依赖的类,比如A类中引用了B类,那么加载A类的时候就会使用加载A的加载器加载B,而且每一个我们编写的类都是由 系统类加载器(AppClassLoader)
加载的,那
知道SPI的同学可能就知道JDBC、JAXP,不了解的下面一节会讲到,他们都是基于SPI实现的,基本上说就是JDK提供接口,服务商提供不同的实现(jar包),当我们使用这些SPI接口时,我们都要导入相应的jar包到classpath下的指定目录可能为lib,mysql-connectorJ等,但是我们的SPI接口是在rt.jar中的,是由启动器类为我们加载的,那么如果根据 依赖规则和双亲委派模型
,JVM会使用加载该接口类的启动器加载器来加载我们的接口实现类,但是我们的SPI的不同实现类却在classpath下,这里是启动器类加载器加载不到的, classpath只能由系统类加载器或者自定义加载器加载
,那么这样就会导致无法加载SPI接口实现类,所以 双亲委派模型
就不能在这起到合适的作用,我们就只能想办法去让 系统加载器来支持加载SPI实现类
,于是出现了上下文类加载器
可能有人会说直接把各个厂商的实现放入对应的接口类所在包里不就好了,乍一看这么做是可以解决问题,但是你要知道的是无论在设计模式还是JDK中都是 面向扩展,对修改关闭的
,这样做不仅违背了设计模式还会让JDK包变的务必庞大
它改变了父加载器的加载方式,也就是破坏了双亲委托模型,它让父加载器可以使用当前线程的 Thread.currentThread().getContextClassLoader()`类加载器获取到加载classpath下类的加载器,使用该加载器去加载类,这就改变了父加载器不能使用子加载器加载的类的情况
根据双亲委派模型传递顺序,父类加载器加载不了才会交给子类加载器,所以它自然看不到并无法加载子类加载器加载的类,智慧的JDK开发者发现了这一点,想到了一个 线程中的类加载器
,就可以通过线程的上下文类加载器来让父加载器可以访问子加载器所加载的类,就相当于把系统类加载器放在当前线程的上下文类加载器中,当父加载器需要获取子类加载器加载的类时,就可以通过这种方式获取
源码文档写道:
If not set, the default is the ClassLoader context of the parent Thread. The context ClassLoader of the primordial thread is typically set to the class loader used to load the application.复制代码
我们后面就根据一些源码分析和案例使用来看一看上下文类加载器到底有多么强大的功能,竟然可以破坏双亲委派模型
SPI—Service Provider Interface,服务提供接口,像JDBC加载就是使用了spi,服务提供商使用spi扩展接口功能,类似根据jdk提供的一个接口不同服务提供商实现不同的接口实现,封装成一个jar包,我们通过导入这个jar包就可以使用服务提供商提供的该不同接口实现对应功能,通过ServiceLoader类加载不同服务提供商的实现—你可以简单理解为 策略模式
官方文档写的: 是一个加载服务提供商提供的服务实现的设备
A simple service-provider loading facility.复制代码
使用:官方文档写到:
A service provider is identified by placing a provider-configuration file in the resource directory META-INF/services. The file's name is the fully-qualified binary name of the service's type. The file contains a list of fully-qualified binary names of concrete provider classes, one per line. 复制代码
如: JDBC->文件名:java.sql.Driver 文件内容:com.mysql.cj.jdbc.Driver
JDK就会去找到java.sql.Driver这个接口,然后找到文件内容中的在jar包中对应的com.mysql.cj.jdbc.Driver类作为该接口的实现
同一个服务的不同提供商将根据jdk SPI规范编写符合规范的实现类(对类没有要求,只需要实现接口就好了,但是需要添加 META-INF/services/服务限定名
文件,在其中每一行写服务提供商提供的类相应的在jar包目录下的全限定名)
下面我们自己实现一个spi服务看一下它到底是如何运作的,写完之后我们再看源码
top.dzou.jvm.spi
package top.dzou.jvm.spi; public interface TestInterface { void saySomething(); } 复制代码
top.dzou.jvm.spi.impl
package top.dzou.jvm.spi.impl; public class ConcreteImpl1 implements TestInterface { @Override public void saySomething() { System.out.println("I am first service provider interface impl;"); } }复制代码
package top.dzou.jvm.spi.impl; public class ConcreteImpl2 implements TestInterface { @Override public void saySomething() { System.out.println("I am second service provider interface impl;"); } }复制代码
META-INF/services
,配置文件名为接口包路径全限定名top.dzou.jvm.spi.TestInterface` top.dzou.jvm.spi.impl.ConcreteImpl1 top.dzou.jvm.spi.impl.ConcreteImpl2复制代码
ServiceLoader
public class TestSpi { public static void main(String[] args) { //Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader().getParent()); ServiceLoader<TestInterface> loader = ServiceLoader.load(TestInterface.class); Iterator<TestInterface> iterator = loader.iterator(); System.out.println("current class loaded by :"+TestSpi.class.getClassLoader()); System.out.println("current thread loader :"+Thread.currentThread().getContextClassLoader()); System.out.println("service interface loader :"+loader.getClass().getClassLoader()); while(iterator.hasNext()){ TestInterface next = iterator.next(); next.saySomething(); } } } 输出: current class loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2 current thread loader :sun.misc.Launcher$AppClassLoader@18b4aac2 service interface loader :null I am first service provider interface impl; I am second service provider interface impl;复制代码
如果我们把main函数第一行之前加上一行
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader().getParent());复制代码
输出为
current class loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2 current thread loader :sun.misc.Launcher$ExtClassLoader@266474c2 service interface loader :null复制代码
解释:
你可以把我们写的接口实现看成是某个服务商提供者编写的jar包的类,把接口看成是JDK提供的服务接口,然后在jar包中的resource目录下的 META-INF/services
中编写了一个与JDK提供服务接口全限定名相同的配置文件,在其中配置了两个具体实现类的类全限定名,就可以通过ServiceLoader去使用这两个类作为JDK接口的实现类,我们在测试类中测试的结果可以看到除了ServiceLoader类由启动类加载器加载,线程和测试类都是通过系统类加载器加载的;
但是
当我们设置了扩展类为线程上文文类加载器的时候,可以看到打印结果是我们自己编写的服务接口实现没有被加载,那这是为什么?
答:很简单,因为ServiceLoader是通过上下文类加载器获取到系统类加载器的引用,通过系统类加载器来帮助我们实现访问服务实现的类,但是现在我们的上下文类加载器为扩展类加载器,显然扩展类加载器是加载和访问不了我们自己编写的服务实现类,所以自然没有打印处加载的信息,更没有去调用方法
我们通过上下文类加载器和自定义SPI实现大致已经知道SPI是怎么运作的了,我们下面看一下它的源码
因为sun公司源码有些是不对外开放的,所以我们看一下反编译的源码就好了,大致都能理解
private static final String PREFIX = "META-INF/services/";复制代码
public static <S> ServiceLoader<S> load(Class<S> var0) { ClassLoader var1 = Thread.currentThread().getContextClassLoader();//核心方法 return load(var0, var1); }复制代码
private ServiceLoader(Class<S> var1, ClassLoader var2) { this.service = (Class)Objects.requireNonNull(var1, "Service interface cannot be null"); this.loader = var2 == null ? ClassLoader.getSystemClassLoader() : var2; this.acc = System.getSecurityManager() != null ? AccessController.getContext() : null; this.reload(); }复制代码
loader
的地方 ServiceLoader维护了一个内部类 LazyIterator
实现了 Iterator
接口作为使用服务提供商在配置文件中编写的所有服务实现类的迭代器,看一下 hasNextService
方法,我把关键部分留了下来
private boolean hasNextService() { //关键是这里,反编译把常量直接加载过来了 if (this.configs == null) { try { String var1 = "META-INF/services/" + this.service.getName();//这里service就是 if (this.loader == null) { this.configs = ClassLoader.getSystemResources(var1);//一般不会来到这,如果出现异常来到这也要把loader设置为系统类加载器 } else { this.configs = this.loader.getResources(var1);//使用系统类加载器根据jar包中路径获取资源,也就是使用服务实现 } } catch (IOException var2) { ServiceLoader.fail(this.service, "Error locating configuration files", var2); } //下面使用迭代器,负责判断是否有其他服务实现 while(this.pending == null || !this.pending.hasNext()) { if (!this.configs.hasMoreElements()) { return false; } this.pending = ServiceLoader.this.parse(this.service, (URL)this.configs.nextElement()); } this.nextName = (String)this.pending.next(); return true; } }复制代码
private S nextService() { String var1 = this.nextName;//拿到下一个服务类的类全限定名 this.nextName = null; Class var2 = null; try { var2 = Class.forName(var1, false, this.loader);//使用反射加载服务实现,loader为系统类加载器,var1为nextName就是服务类全限定名 Object var3 = this.service.cast(var2.newInstance()); ServiceLoader.this.providers.put(var1, var3);//加载成功放入Maop中 return var3; } }复制代码
里面有这样一段代码
try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader);复制代码
它首先就是获取系统类加载器作为Launcher中把保存的loader引用,因为它是JDK最下面的类加载器。可以通过getParent方法获取上册加载器;并且调用了 Thread.currentThread().setContextClassLoader方法把系统类加载器设置为当前线程的上下文类加载器
SPI原理和ServiceLoader的源码讲完我们下面看一下SPI对服务接口的实际使用
我们一般通过 Class.forName("com.mysql.cj.jdbc.Driver");
先使用加载当前类的加载器(也就是系统类加载器)加载该classpath下的mysql驱动
现在我们再来看这张图片就能会容易理解了,配置文件的内容你可能也已经想到了,就是JDBC的mysql驱动
com.mysql.cj.jdbc.Driver
或者 com.mysql.jdbc.Driver
public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } } }复制代码
我们在通过Class.forName加载完该Driver时会自动初始化该类,就会执行static语句块,自然就会加载引用的DriverManger,根据双亲委托模型,把加载DriverManager的任务交给启动器类加载器
static { loadInitialDrivers(); }复制代码
我们看一下它静态块中执行的初始化Driver的方法
private static void loadInitialDrivers() { String var0 = (String)AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers");//如果存在系统的jdbc driver则返回,一般不存在,需要加载 } }); AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader var1 = ServiceLoader.load(Driver.class);//ServiceLoader加载java.sql.Driver Iterator var2 = var1.iterator(); while(var2.hasNext()) {//通过hasNext调用hasNextService方法拿取配置文件中指定的类的资源 var2.next();//调用nextService方法会通过Class.forName()加载这个类 } } return null; } }); if (var0 != null && !var0.equals("")) {//如果System.getProperty("jdbc.drivers");中有驱动 String[] var1 = var0.split(":"); String[] var2 = var1; int var3 = var1.length; for(int var4 = 0; var4 < var3; ++var4) { String var5 = var2[var4]; println("DriverManager.Initialize: loading " + var5); Class.forName(var5, true, ClassLoader.getSystemClassLoader());//尝试加载System.getProperty中的驱动 } } }复制代码
这么一看进行了很多次Class.forName()加载驱动,那我们为什么还需要手动调用 Class.forName("com.mysql.cj.jdbc.Driver");
?是不是可以不手动调用这一步?
答案是可以的,我们手动调用这步是因为JDK以前还不支持这种做法,需要调用,但是后面版本的JDK中可以不需在调用这一句了,因为只要在classpath中,它就会在loadInitialDrivers中调用next中调用nextService方法中调用了这句Class.forName()
String var0: 驱动类全限定名
Properties var1: 包含数据库连接参数的配置信息
Class var2: 反射拿到的调用getConnetion方法的类
关键代码如下
private static Connection getConnection(String var0, Properties var1, Class<?> var2) throws SQLException { ClassLoader var3 = var2 != null ? var2.getClassLoader() : null;//拿到加载调用类的类加载器,一般为系统类加载器 Class var4 = DriverManager.class; synchronized(DriverManager.class) { if (var3 == null) { var3 = Thread.currentThread().getContextClassLoader();//如果不是系统类加载器就设置为当前线程的1类加载器,也就是存储的系统类加载器的引用 } } Iterator var5 = registeredDrivers.iterator(); while(true) { while(var5.hasNext()) {//调用迭代器来加载驱动 DriverInfo var6 = (DriverInfo)var5.next(); if (isDriverAllowed(var6.driver, var3)) {//关键在这里 Connection var7 = var6.driver.connect(var0, var1); if (var7 != null) { return var7; } } } } }复制代码
出现这种情况的原因:
1.上下文类加载器被设置为了高层的类加载器而不是系统类加载器
2.线程被切换了,当前线程的上下文类加载器不是加载调用类的类加载器
private static boolean isDriverAllowed(Driver var0, ClassLoader var1) { boolean var2 = false; if (var0 != null) { Class var3 = null; try { var3 = Class.forName(var0.getClass().getName(), true, var1); } catch (Exception var5) { var2 = false;//如果异常发生,表示无法由var0加载var1,命名空间不同 } var2 = var3 == var0.getClass();//否则只需要判断加载的类和var0驱动类是否是一个类 } return var2; }复制代码
我们在上述情况下思考一下双亲委托模型可以实现吗?
显然不行,所以tomcat创建了自己的一套加载模型,如下:
common类加载器
就是负责加载服务器和应用程序都可以共享的类库,如classpath下的lib目录 catalina类加载器
负责加载服务器独立的类库,为了安全性不与应用程序共享的类库 shared类加载器
就负责加载应用程序之间共享的类库,像是Spring这样的 WebApp类加载器
加载单个应用程序独立的类库,对其他应用程序不可见,如webapp下类库 jsp类加载器
负责jsp文件加载成servlet类,它需要解决 热更新
的问题 我们知道一般加载过程,创建一个JSP页面,启动服务器时由加载器加载成servlet类字节码文件,但是当你JSP内容修改了以后,就相当于类文件被修改了,这个时候我们只能重新启动应用程序来再次加载这个类来实现修改后的更新,但是如果是这样的话就没有人使用 JSP
了
tomcat考虑到了这一点,提出了一种 一个类加载器对应一个JSP文件
的实现方法
为了实现不同应用程序隔离,服务器和应用程序隔离,就不同在使用双亲委托模型,它会把所有加载交给父类,而保证每个类有且仅由一个,所以tomcat不得不 破坏双亲委托模型
,但它只是没有遵循交给上层加载的规定,加载模型还是自上而下的
Tomcat决定把webapp目录下的类由自己的WebappClassLoader加载,不委托给父类加载器,然后通过舞弊的 上下文类加载器
来实现父加载器对子类加载器加载的类的访问与可见性