类加载器 classpath 摘要
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
==========================================================
https://blog.csdn.net/qq_21508059/article/details/81843498
要实现热加载,必须知道以下几点:
- 对于同一个全限定名,一个ClassLoader中只允许加载一次该Class实例
- 全限定名相同,类加载器不同,Class相等
- 类加载器默认采用双亲委派机制,如果要实现热加载的类在classpath路径下,自定义类加载器要打破双亲委派机制
问题:A-被自定义加载的类,A中new B,是否可以;若B已经被系统加载器加载,自定义加载器是否会更新
B可以被加载,基于双亲委托,若B存在于classpath中,则会被系统类加载器加载,若不能被系统类load,则由我们的自定义加载器加载
若要A,B强行由自定义加载器热加载,则:
1)使类A,B不在系统类加载器AppClassLoader的可见范围(classpath下),自定义加载时,仍然遵循双亲委派,去父加载器找,找不到再亲自上
2)使用findclass,或覆盖loadclass,打破双亲委派
第2)种方式对于A首次引用B(new方式)会有问题,我们是我可以新建classloader显式findclass A,但是B依据双亲委派很是由系统类加载器加载的,如果强行将所有B的引用改为显式findclass加载,对代码会有侵入https://blog.csdn.net/mdreamlove/article/details/79212420
实践位于:https://www.cnblogs.com/silyvin/articles/10269681.html
https://www.cnblogs.com/silyvin/articles/10274914.html
1.31补充,其实还有一种运行期加载外部jar包的方法:https://blog.csdn.net/zsllxbb/article/details/49902661
添加一个classpath路径,Classpath,在于告诉Java执行环境,在哪些目录下可以找到您所要执行的Java程序所需要的类或者包,或者xml,properties资源文件。
URL url= file.toURI().toURL();//将File类型转为URL类型,file为jar包路径
//得到系统类加载器
URLClassLoader urlClassLoader= (URLClassLoader) ClassLoader.getSystemClassLoader();
//因为URLClassLoader中的addURL方法的权限为protected所以只能采用反射的方法调用addURL方法
Method add = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class });
add.setAccessible(true);
add.invoke(urlClassLoader, new Object[] {url });
Class c=Class.forName("类名");
或者
Class c=urlClassLoader.loadClass("类名");
此种方法是得到系统类加载器,利用该加载器加载指定路径下的jar包,此种方法与java命令中的javac -cp是同等效果,都能在当前运行环境中改变CLASSPATH,所以利用该方法加载jar包后,在程序任一地方都能加载该jar包中的类,调用其方法。
这里getSystemClassLoader,实际情况中,最好应为Thread.currentThread().getContextClassLoader()
因为主加载器未必是系统类加载器,有可能是上层生态自定义的,如saturn自己就定义了自己的类加载器(parent class loader com.vip.saturn.job.executor.JobClassLoader@5fd764ee),如果用系统类加载器,那么就变成平行关系非父子关系了,就不能使用saturn预加载的类了
当然,这种方式是不能热加载的,仅能运行期加载,但不能卸载
https://blog.csdn.net/l1028386804/article/details/53453287
有时候我们会遇到热加载的需求,即在修改了类的代码后,不重启的情况下动态的加载这个类
看到这个问题,我想到有两种方式来实现:(能力有限,不知道还有没有其他的方案)
1:把原来的类信息卸载掉,然后重新加载此类。
2:新建一个类加载器(new),重新加载此类,不管原来的类信息,等待垃圾回收它。
第一种方案是行不通的,因为java并没有给我们提供手动卸载类信息的功能,也就是运行时方法区内的类信息是不能卸载的,除非这个类已经不再使用,这时GC会自动把它回收掉。所以我们只能通过第二种方案来实现。
几个问题
在使用这种方案实现之前我们考虑几个问题,注意:下面的问题是在这种方案下提出的,也在这种方案下回答,不适用与其他方案。
1:是不是所有的类都能进行热加载呢?
我们程序的入口类都是系统类加载器加载的,也就是AppClassLoader加载的。当你重新使用系统类加载器加载这个类的时候是不会被重新加载的。因为虚拟机会检测这个类是否被加载过,如果已经被加载过,那么就不会重新加载。所以由系统类加载器加载的类,是不能进行热加载的。只有使用我们自定义的类加载器加载的类才能热加载。
2:自定义类加载器的父加载器应该是哪个加载器?
我们自定义类加载器的父加载器有两种选择,一个是系统类加载器(AppClassLoader),另一种是扩展类加载器(ExtClassLoader)。首先我们要知道,扩展类加载器是系统类加载器的父加载器。我们先考虑第一种,如果父加载器是系统类加载器(当然如果你不指定父加载器,默认就是系统类加载器),那么会出现一个现象,这个动态加载的类不能出现在系统类加载器的classpath下。因为如果在系统类加载器的classpath下的话,当你用自定义类加载器去加载的时候,会先使用父类加载器去加载这个类,如果父类加载器可以加载这个类就直接加载了,达不到热加载的目的。所以我们必须把要热加载的类从classpath下删除。除非覆盖loadclass,或运行期使用findclass,打破双亲委派
在考虑第二种,如果父加载器是扩展类加载器,这时候热加载的类就可以出现在classpath下,但又会出现另一种现象,这个类中不能引用由系统类加载器加载的类。因为这时候,自定义类加载器和系统类加载器是兄弟关系,他们两个加载器加载的类是互不可见的。这个问题貌似是致命的。除非热加载的类中只引用了扩展类加载器加载的类(大部分javax开头的类)。所以我们自定义的类加载器的父加载器最好是系统类加载器。
3:热加载的类要不要实现接口?
要不要实现接口,要根据由系统类加载器加载的类A中是否有热加载类B的引用。如果有B的引用,那么加载A的时候,系统类加载器就会去加载B,这时候B不在classpath下,就会加载报错。这时候我们就需要为B定义一个接口,A类中只有接口的引用。这样我们使用系统类加载器加载接口,使用自定义类加载器加载B类,这样我们就可以热加载B类。如果A中没有B的引用的话,就灵活多了,可以实现接口,也可以不实现接口,都可以进行热加载。
解决了这三个问题后,实现代码就已经在我们心中了。下面直接看代码:。
结合saturn调用任务的实现,saturn对任务类,是没有任何隐式调用的
当然基于公司规范,也建议提供一个接口或抽象类,其它开发者来继承:https://www.cnblogs.com/silyvin/articles/10274914.html
主加载器加载接口(或抽象类),子(热)加载器加载实现类
如果不采用接口-实现分离类加载器形式,则取得class后,可用反射调用函数:https://blog.csdn.net/guim_007/article/details/65953818
https://www.cnblogs.com/jxrichar/articles/4930996.html
热部署有一个缺陷,就是很容易导致内存泄露, 并且不是很容易从代码层次避免. 所以产品环境一般不推荐启用热部署
JVM只提供了加载Class的方法,没有提供卸载的方法.所以针对这种情况就需要使用新的ClassLoader来重新加载新的Class来实现热部署. 大部分情况下旧的ClassLoader及所Loader的对象就会形成一个孤岛,稍后就会被回收.
但是事情不是绝对的,如果恰巧有别的ClassLoader加载了一个对象,并且这个对象引用了孤岛中的某个对象,那么孤岛将不再是孤岛,这个时候内存泄露就会发生了.比如Log4j和热部署加一块,就可能会出现内存泄露
1)通过实现一个Classloader,加载指定的Class并实例化,然后关联到LinkedList上面.
2)监控指定Class的变化,如果有变化就new一个新的ClassLoader对象,然后重新加载Class并实例化对象,然后关联到LinkedList上面.
3)观察新旧ClassLoader生成出来的对象,是否可被垃圾回收.如果不能垃圾回收,在Tomcat进行热部署也会有同样的事情了.
同一个ClassLoader(并不仅仅指的是同一个Class,而是同一个实例),具体来说就是如果一个JVM里面, 如果new了多个ClassLoader,那么他们是不同的ClassLoader.这样当他们Loader相同Class时,这些生成的实例,虽然包名,类名想等,但是却不可能equals.
https://blog.csdn.net/kypfos/article/details/3160764
类加载器的规则有三
1. 一致性规则:类加载器不能多次加载同一个类
2. 委托规则 :在加载一个类之前,类加载器总参考父类加载器
3. 可见性规则:类只能看到由其类加载器的委托加载的其他类,委托是类的加载器及其所有父类加载器的递归集。
3. 几个问题
1) 为什么要在单独的工程里放置 CatImpl 类(重要)
主要是为了编译成的 CatImpl 类对于 TestHotDeployInf 的系统加载类不可见,就是不能放在 TestHotDeployInf 的程序的 classpath 中。
这个问题可以说大,本应该提高一个层次来说明它。前面提过标准 Java 启动器加载器层次中有三个加载器,而在上面的 Client.java 中,我们看到用了一个自定义的 cl = new URLClassLoader(externalURLs) 类加载器来加载 com.unmi.CatImpl。也就是标准的类加载器又多了一层,这里估且把它叫做应用程序加载器(AppClassloader)。
根据委托规则,执行 Client 时,要加载 com.unmi.CatImpl 时会首先委托加载 Client 类本身的系统加载器加载。如果编译出的 CatImpl.class 放在 Cat.class 相同的位置,那么就由系统加载器来加载 com.unmi.CatImpl,自定义加载器 cl 是没机会了。所以必须放在外面让系统加载器看不到 com.unmi.CatImpl 类。(除非覆盖loadclass,或运行期使用findclass,打破双亲委派)
再依据一致性规则,如果系统加载器能加载了 com.unmi.CatImpl 类,以后你怎么修改 CatImpl 类,替换掉原来的类,内存中总是最先加载的那个 com.unmi.CatImpl 类版本。因为类只会加载一次。而用自定义的 cl 可不一样了,每次执行 cl.loadClass("com.unmi.CatImpl") 时都是用的一个新的 ClassLoader 实例,所以不受一致性规则的约束,每次都会加载最新版本的 CatImpl 类。
2) 关于类的卸载的问题
上一条讲了加载 com.unmi.CatImpl 时,每次都 new 了一个新了 ClassLoader 实例,每次都加载最新的 CatImpl 类,那就引出了不再使用的 ClassLoader 实例和早先旧版本的 CatImpl 类实例的回收问题。在多数 JVM 中,它们如同普通的 Java 对象一样的处理,当它们无从触及时被当作垃圾被收集掉。
应该看看常见应用服务器(如 Tomcat) 的类加载器层次
对于可见性规则可以举两个例子:
1) 对于标准的类加载器层次,放在 jre/lib/ext 中的类(由扩展类加载器加载)可以让放在 classpath 下的类(由系统类加载器加载) 访问到,反过来就不行了。因为系统类加载器可以访问其父加载器扩展类加载器的委托
2) 应用服务器中不同的 Web 应用中类不能相互访问,因为它们是由不同的类加载器加载的,且是在并行结构中。而在企业应用程序中的 WAR 包使用到 EJB 包和其他工具包,因为加载 WAR 包的类加载层是在加载 EJB 包和其他工具包的类加载器的下层。 子加载器能访问父加载器的委托
https://blog.csdn.net/javazejian/article/details/73413292#%E5%90%AF%E5%8A%A8bootstrap%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8
请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器
loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,
正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载
当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
类加载器默认采用双亲委派机制,如果要实现热加载的类在classpath路径下,自定义类加载器要打破双亲委派机制,直接覆盖loadclass,或直接使用findclass
同时应该知道的是findClass方法通常是和defineClass方法一起使用的
defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的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);
}
}
其实无论是ExtClassLoader还是AppClassLoader都继承URLClassLoader类,因此它们都遵守双亲委托模型,这点是毋庸置疑的
启动类加载器,由C++实现,没有父类。
拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
自定义类加载器,父类加载器肯定为AppClassLoader。
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
从前面双亲委派模式对loadClass()方法的源码分析中可以知,在方法第一步会通过Class<?> c = findLoadedClass(name);
从缓存查找,
如果调用父类的loadClass方法,结果如下,除非重写loadClass()方法去掉缓存查找步骤,不过现在一般都不建议重写loadClass()方法
显示加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。而隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。 new是隐式加载
实现自定义类加载器需要继承ClassLoader或者URLClassLoader,继承ClassLoader则需要自己重写findClass()方法并编写加载逻辑,继承URLClassLoader则可以省去编写findClass()方法以及class文件加载转换成字节码流的代码。
由于启动类加载器、拓展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将有自定义类加载器加载,即调用findClass()方法进行加载。如果继承URLClassLoader实现,那代码就更简洁了
由于JVM在加载类之前会检测请求的类是否已加载过(即在loadClass()方法中调用findLoadedClass()方法),如果被加载过,则直接从缓存获取,不会重新加载。注意同一个类加载器的实例和同一个class文件只能被加载器一次,多次加载将报错,因此我们实现的热部署必须让同一个class文件可以根据不同的类加载器重复加载,以实现所谓的热部署。实际上前面的实现的FileClassLoader和FileUrlClassLoader已具备这个功能,但前提是直接调用findClass()方法,而不是调用loadClass()方法,因为ClassLoader中loadClass()方法体中调用findLoadedClass()方法进行了检测是否已被加载,因此我们直接调用findClass()方法就可以绕过这个问题,当然也可以重新loadClass方法,但强烈不建议这么干。利用FileClassLoader类测试代码如下:
//加载指定的class文件,调用loadClass() Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj"); Class<?> object2=loader2.loadClass("com.zejian.classloader.DemoObj"); System.out.println("loadClass->obj1:"+object1.hashCode()); System.out.println("loadClass->obj2:"+object2.hashCode()); //加载指定的class文件,直接调用findClass(),绕过检测机制,创建不同class对象。 Class<?> object3=loader.findClass("com.zejian.classloader.DemoObj"); Class<?> object4=loader2.findClass("com.zejian.classloader.DemoObj"); System.out.println("loadClass->obj3:"+object3.hashCode()); System.out.println("loadClass->obj4:"+object4.hashCode()); /** * 输出结果: * loadClass->obj1:644117698 loadClass->obj2:644117698 findClass->obj3:723074861 findClass->obj4:895328852
https://blog.csdn.net/luman1991/article/details/75105257
// class字节码所在的位置. String dir = "file:/E:\\datacloudWorkspace8\\JbossWebTest\\build\\classes\\"; URL url = new URL(dir); OneURLClassLoader oucl = new OneURLClassLoader(new URL[] { url }); // 用类加载器加载kite.jvm.Constant并返回它的class对象. Class c = oucl.findClass("kite.jvm.Constant");// 直接加载,不依靠父委托机制
https://blog.csdn.net/u013412772/article/details/80927208
模块隔离功能:每个模块使用一个类加载器进行加载
https://blog.csdn.net/mdreamlove/article/details/79212420
子加载器的命名空间包含所有父类加载器的命名空间,因此由子加载器加载的类能看见父类加载器加载的类。由父加载器加载的类不能看见子加载器加载的类。
当执行Sample类的构造方法中的new Dog()语句时,JVM需要先加载Dog类,到底用哪个类加载器家在呢?从上述的打印结果中可以看出,加载Sample类的loader1还加载了Dog类,JVM会用Sample类的定义类加载器去加载Dog类,
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
学习这个能干啥
- 热加载/热部署
- 远程通信协议加载字节码
- 实现字节码的加密解密,避免jar包、class文件流出,反编译后重要账户密码泄露
- 代码隔离
- (半个)配合JarInputStream或URL伪装纯内存加载jar包,以实现jar包解密后内存中解密直接加载
加载顺序总结
MyClassLoader查看本加载器缓存,native findLoadedClass
MyClassLoader交给父加载器loadclass
MyClassLoader调用本类的findClass
MyClassLoader调用本类的defineClass