上一篇我们总结了类加载器的基本原理和与应用程序相关的ClassLoader,并提到了双亲委派模式。本篇继续探讨类加载器的双亲委派模式,以及如何破坏双亲委派模式达到加载底层类的目的。
1.双亲委派模式的问题
我们回顾一下原来的应用程序的ClassLoader的加载模式:
除了顶层的ClassLoader,每一个ClassLoader都有一个父级ClassLoader,当加载一个类的时候,是自底向上查找,自顶向下查找,所以会出现一个问题,就是顶层的ClassLoader无法加载底层ClassLoader的类,即在启动ClassLoader中,无法启动和加载AppClassLoader里的任何一个类,也没有办法生成这个类的实例对象。
其实双亲委派的做法本意是:
1)Java类随着它的类加载器一起具备了一种带有优先级的层次关系
2)防止自定义类与系统核心类重名,导致程序混乱
但是这种顶层的类加载器不能加载底层的类的方式,会引起一些特殊场景下的加载问题。
2.双亲委派模式的问题场景
最典型的一个例子就是就是,JDK自带的rt.jar是顶层加载器(BootStrap ClassLoader启动加载器)可加载的,但是在该jar包中的javax.xml.parsers包中定义了xml解析的类接口SPI(Service Provider Interface),但是我们去实现SPI的实现类的时候,是我们自己自定义的类,这些类都在应用类加载器(App ClassLoader)中。
此时我们就可以看到两个问题:
1)SPI接口本身,以及产生这些类的工厂类本身,都是在rt.jar中的,是被顶层加载器(BootStrap ClassLoader启动加载器)可加载的。
2)SPI接口的实现类,属于用户开发的应用层类,它们是由应用类加载器(App ClassLoader)定义的实例类。
此时就要求启动加载器(BootStrap ClassLoader)要能访问应用类加载器(App ClassLoader)中的内容。
但是根据上面的双亲委派模式,是无法从顶层加载器去加载底层加载器中的类对象的,但是在这种场景下,确实需要从顶层加载器去加载SPI接口的实现类,此时衍生了以下解决方案:
Thread.setContextClassLoader(ClassLoader cl);
Thread.getContextClassLoader();
这里的意思就是,为一个线程设置一个“上下文”的类加载器(Context上下文),以及获取上下文类加载器。该方法的提出就是为了解决让顶层类加载器去加载底层类加载器的内容。它的基本思想就是,在顶层类加载器中,传入底层类加载器的实例。说白了,该“上下文类加载器”其实不是一个真实的类加载器,而是一个角色,它的作用就是设置一个底层的类加载器,作为顶层的一个“上下文类加载器”。
下面的代码就是rt.jar包中javax.xml.parsers.FactoryFinder的源代码,展示了如何在启动类加载器加载AppLoader的类:
static private Class getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
try {
if (cl == null) {
if (useBSClsLoader) {
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
} else {
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}
else {
return cl.loadClass(className); //使用上下文ClassLoader
}
}
}
else {
return cl.loadClass(className);
}
}
catch (ClassNotFoundException e1) {
if (doFallback) {
// Use current class loader - should always be bootstrap CL
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
}
…..
javax.xml.parsers.FactoryFinder的功能就是,帮启动加载器(BootStrap ClassLoader)加载一个应用类加载器(App ClassLoader)才能加载的类。其中“return cl.loadClass(className)”就是使用上下文ClassLoader的核心,这里的cl就是上下文的类加载器,此时将应用类加载器(App ClassLoader)赋值到上下文加载器中即可。
所以使用线程上下文类加载器,就可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。
3.双亲委派模式的破坏
双亲委派模式是JVM的默认模式,但是不是必须这么做,因为JVM并不强迫我们这么做。例如:
(1)Tomcat
Tomcat有自己的类加载模式,其中的WebappClassLoader类加载器,它会先加载自己的class,找不到再委托parent。
(2)OSGI
OSGI组件开发模式的特点就是可以热加载,模块化比较强。在热加载过程中,各个组件的类可能会被随时停止,或者随时启动加入,它就要求类加载是按照需求来的。所以OSGI对类加载模式进行了很大、很复杂的改造,有一套查找类的算法和机制。它的Classloader形成了一个网状结构,会根据需要自由加载Class,也是完全不符合双亲委派模式的。
4.破坏双亲委派模式实例
下面的实例就是从底层ClassLoader加载类:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// First, check if the class has already been loaded
Class re=findClass(name);
if(re==null){
System.out.println("无法载入类:"+name+"需要请求父加载器");
return super.loadClass(name,resolve);
}
return re;
}
上面的方法中可以看到,在加载类的时候,首先是在自己的ClassLoader中进行查找并加载,如果找不到并加载不出来,才回去父级类加载器中寻找并加载。
其中的findClass方法:
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class clazz = this.findLoadedClass(className);
if (null == clazz) {
try {
String classFile = getClassFile(className);
FileInputStream fis = new FileInputStream(classFile);
FileChannel fileC = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel outC = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
//省略部分代码...
fis.close();
byte[] bytes = baos.toByteArray();
clazz = defineClass(className, bytes, 0, bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return clazz;
}
在该方法中,对类进行了查找、定义以及加载。首先判断该类是否已经加载过了,如果该类没有被加载,则尝试读取类文件,并使用输入流将该类的二进制byte数据读出来,并调用defineClass将该类真真实实的定义起来。此时就达到了从底层加载类的实例的效果。
5.自定义类加载器
自定义一个类加载器,我们需要继承ClassLoader,并且重写父类的findClass()方法即可。
样例代码:
package cn.com.classloader.test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
public class MyClassLoader extends ClassLoader{
public static final String drive = "F:/";
public static final String fileType = ".class";
public static void main(String[] args) throws Exception {
MyClassLoader loader = new MyClassLoader();
Class<?> objClass = loader.loadClass("HelloWorld", true);
Object obj = objClass.newInstance();
System.out.println(objClass.getName());
System.out.println(objClass.getClassLoader());
System.out.println(obj.getClass().toString());
}
//重写父类加载器的findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//调用自定义的loadClassData方法,从drive找到相关的类文件并加载
byte[] data = loadClassData(name);
//调用父类defineClass方法,从byte中定义类的实例
return super.defineClass(name,data,0,data.length);
}
//根据类名从指定的区域加载类的byte流
public byte[] loadClassData(String name){
FileInputStream fis = null;
byte[] data = null;
try {
fis = new FileInputStream(new File(drive+name+fileType));//获取文件输入流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int index = 0;
while((index = fis.read()) != -1){
baos.write(index);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
}
这里我们重写了父类加载器的findClass方法,从我们制定的磁盘目录加载相关的类文件的byte数据,给父类的defineClass方法生成类的实例。
我们在F盘下放置一个HelloWorld.java的类,其中的代码如下:
public class HelloWorld{
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
然后执行javac进行编译,得到class文件:
运行代码,得到结果:
我们来思考一下,我们重写的findClass方法被父类ClassLoader用在什么地方呢?我们打开ClassLoader的loadClass()方法就知道了:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name))
{
// 第一步先检查这个类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null)
{
long t0 = System.nanoTime();
try
{
//parent为父加载器
if (parent != null)
{
//将搜索类或资源的任务委托给其父类加载器
c = parent.loadClass(name, false);
} else
{
//检查该class是否被BootstrapClassLoader加载
c = findBootstrapClassOrNull(name);
}
}
catch (ClassNotFoundException e)
{
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null)
{
//如果上述两步均没有找到加载的class,则调用findClass()方法
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve)
{
resolveClass(c);
}
return c;
}
}
在上述方法中,首先会先检查该类是否已被加载,如果没有被加载,则委托其父类进行加载,如果没有父类则检查最顶层的Bootstrap ClassLoader是否加载了该类。倘若前面都没有,则该类加载器执行自己的findClass方法,查找和加载该类。而我们自定义类加载器的核心就是这个查找和加载该类,是去我们自己定义的区域内加载。
我们由此可以推断出,所有的类加载器的findClass方法都是根据这个类加载器的特性,去不同的地方来加载类的。例如App ClassLoader就去应用层的classpath中加载类,而顶层的BootStrap ClassLoader就去JDK的classpath中区加载类。
6.使用自定义类加载器实现热替换
在PHP中就有热替换的特性,例如我们在PHP服务器运行的过程中,替换掉一个PHP文件,以后的运行就以替换后的PHP文件进行执行。而除了OSGI,传统的JAVA程序在容器中运行时(例如Tomcat),如果替换了class文件,此时是不会立刻生效,而需要重启容器才可以被重新加载。
下面我们使用破坏双亲委派模式的类加载方式,来实现一个热启动的效果。
首先编写一个运行中要被替换的类Worker:
public class Worker {
public void sayHello(){
System.out.println("version:A");
}
}
这里输出一个"version:A"的语句,来证明该类的版本(我们可以替换为打印"version:B"的一个新类)。
因为JVM提供的类加载器,只能加载指定目录的jar和class,如果我们想加载其他位置的类或jar时。需要编写我们自己的ClassLoader来加载。实现一个ClassLoader,并指定这个ClassLoader的加载路径。有两种方式:
方式一:继承ClassLoader,重写父类的findClass()方法。
方式二:继承URLClassLoader类,然后设置自定义路径的URL来加载URL下的类。
这里使用方式二来编写自定义的ClassLoader,将指定的目录转换为URL路径,然后重写findClass方法:
package cn.com.classloader.test;
import java.net.URL;
import java.net.URLClassLoader;
public class HotClassLoader extends URLClassLoader {
public HotClassLoader(URL[] urls) {
super(urls);
}
// 打破双亲模式,保证自己的类会被自己的classloader加载
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class c = findLoadedClass(name);
if (c == null) {
try {
c=findClass(name);
} catch (Exception e) {
}
}
if(c==null){
c=super.loadClass(name, resolve);
}
return c;
}
}
这里我们继承了URLClassLoader类,URLClassLoader类介绍:
URLClassLoader是在java.net包下的一个类。URLClassLoader继承了ClassLoader,可以理解为它将ClassLoader扩展了一下。一般动态加载类都是直接用Class.forName()这个方法,但这个方法只能创建程序中已经引用的类,并且只能用包名的方法进行索引,比如Java.lang.String,不能对一个.class文件或者一个不在程序引用里的.jar包中的类进行创建。
URLClassLoader提供了这个功能,它让我们可以通过以下几种方式进行加载:
* 文件: (从文件系统目录加载)
* jar包: (从Jar包进行加载)
* Http: (从远程的Http服务进行加载)
最后写一个执行类,来进行热启动的模拟:
package cn.com.classloader.test;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class HotClassLoaderMain {
private URLClassLoader classLoader;
private Object worker;
private long lastTime;
private String classDir="G:\\Space-JavaDevelopMent\\JVM-Test\\src\\";
public static void main(String[] args) throws Exception {
HotClassLoaderMain helloMain=new HotClassLoaderMain();
helloMain.execute();
}
private void execute() throws Exception {
while(true){
//监测是否需要加载
if(checkIsNeedLoad()){
System.out.println("检测到新版本,准备重新加载");
reload();
System.out.println("重新加载完成");
}
//一秒
invokeMethod();
Thread.sleep(1000);
}
}
private void invokeMethod() throws Exception {
//通过反射方式调用
//使用反射的主要原因是:不想Work被appclassloader加载,
//如果被appclassloader加载的话,再通过自定义加载器加载会有点问题
Method method=worker.getClass().getDeclaredMethod("sayHello", null);
method.invoke(worker, null);
}
private void reload() throws Exception {
classLoader = new HotClassLoader(new URL[] { new URL(
"file:"+classDir)});
worker = classLoader.loadClass("cn.com.classloader.test.Worker")
.newInstance();
}
//检查文件是否被更新
private boolean checkIsNeedLoad() {
File file=new File(classDir+"cn\\com\\classloader\\test\\Worker.class");
long newTime=file.lastModified();
if(lastTime<newTime){
lastTime=newTime;
return true;
}
return false;
}
}
我们在的eclipse中创建的java工程的src中的代码被编译后,会放在工作空间该工程下的bin文件夹下:
所以这里的类加载路径就是工程所在的bin文件夹下。这里我们使用while死循环来模拟一个java容器中间件,反复执行一个类的sayHello方法,然后检测这个类的class是否被改变,如果改变,重新使用自定义类加载器加载该类。
先将程序跑起来:
我们这里不去停止服务,而是将Worker类的输出语句改为“version B”:
package cn.com.classloader.test;
public class Worker {
public void sayHello(){
System.out.println("version:B");
}
}
此时我们注意控制台的输出语句,Worker类被重新加载了:
此时我们的热启动功能完成啦!!!
这里要注意的是,我们获得类以及类的方法实例,是使用的反射,原因是Worker类是定义在我的eclipse的JVMTest工程的classpath下的,我们自定义的类加载器没有指定父加载器,在JVM规范中不指定父类加载器的情况下,默认采用系统类加载器即AppClassLoader作为其父加载器,所以在使用该自定义类加载器时,需要加载的类不能在类路径中,否则的话根据双亲委派模型的原则,待加载的类会由系统类加载器(这里就是AppClassLoader)加载。所以使用反射去调用类路径中类的方法,就不会被默认的系统类加载器先加载。
该系列所有的源代码在我的github下可下载:https://github.com/ZhuYaoGuang/JVM-Test
标签:10,Java,name,虚拟机,ClassLoader,public,findClass,class,加载 From: https://blog.51cto.com/u_16012040/6166627