ClassLoader类加载器

ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的。ClassLoader通过各种方式,将CLass信息的二进制流读入系统,然后交给JVM进行连接、初始化等操作。

比较两个类是否“相等”

比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。这里所指的相等,包括equals()、isAssignableFrom()、isInstance()、instanceof等。如下所示:

/**
 * 类加载器与instanceof关键字演示
 * 
 * @author xuefeihu
 *
 */
public class ClassLoaderTest {

	public static void main(String[] args) throws Exception {
		ClassLoader myLoader = new ClassLoader() {
			@Override
			public Class<?> loadClass(String name) throws ClassNotFoundException {
				try {
					String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
					InputStream is = getClass().getResourceAsStream(fileName);
					if(is == null) {
						return super.loadClass(name);
					}
					byte[] b = new byte[is.available()];
					is.read(b);
					return defineClass(name, b, 0, b.length);
				} catch (Exception e) {
					throw new ClassNotFoundException(name);
				}
			}
		};
		
		Object obj = myLoader.loadClass("com.moguhu.understanding.jvm.chapter7.section7_4_loader.ClassLoaderTest").newInstance();
		System.out.println(obj.getClass());
		System.out.println(obj instanceof com.moguhu.understanding.jvm.chapter7.section7_4_loader.ClassLoaderTest);
	}

}

运行结果:

class com.moguhu.understanding.jvm.chapter7.section7_4_loader.ClassLoaderTest
false

产生上述的运行结果的原因是:obj是用户自定义类加载器加载的,instanceof后面的com.moguhu.***.ClassLoaderTest是系统应用程序类加载器加载的。

ClassLoader类

如果我们需要自定义ClassLoader,那么JDK中提供了java.lang.ClassLoader抽象类,我们可以通过继承来实现自定义ClassLoader。其主要方法如下:

// 给定一个类名,加载这个类,返回代表这个类的Class对象实例;如果找不到,则抛出异常
public Class<?> loadClass(String name) throws ClassNotFoundException;
// 根据给定的字节流b,定义一个名称为name的类,off和len参数表示Class信息在byte[]中的位置和长度。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError;
// 查找一个类(是自定义时的重要拓展点),如果想改变类加载的形式,可以重载
protected Class<?> findClass(String name) throws ClassNotFoundException;
// 它会去寻找已经加载的类,不可被重载
protected final Class<?> findLoadedClass(String name);

双亲委派模式

双亲委派模式的过程如下:如果一个类加载器收到了类加载请求,他首先不会自己去加载,而是把请求为拍给父类加载器去完成。一层一层的循环下去,直到顶层类加载器。当父类返回无法完成时,子类加载器才会尝试自己加载。如下图所示:

下面介绍一下JDK中提供的几个系统类加载器:

启动类加载器(Bootstrap ClassLoader):它负责加载放在/lib目录中的(按照文件名识别,如果lib下面存放了其他jar文件,则不予加载),或者被-Xbootclasspath参数指定的路径。

拓展类加载器(Extension ClassLoader):它由sun.misc.Launcher$ExtClassLoader实现,负责加载/lib/ext目录中,或者被java.ext.dirs系统变量指定的路径中的类库,开发者可以直接使用。

应用程序类加载器(Application ClassLoader):它由sun.misc.Launcher$AppClassLoader实现,它负责加载用户ClassPath上指定的类库。如果应用程序中没有自定义的类加载器,一般情况下它就是默认的加载器,还有另外一个名字叫:系统类加载器

突破双亲模式

双亲委派模式是JVM的默认行为,事实上可以通过自定义ClassLoader来改变其行为。比如Tomcat和OSGI都有各自独特的类加载顺序。下面看一个自定义ClassLoader的Demo:

继承ClassLoader

/**
 * 自定义ClassLoader,可以从指定Path加载
 * 
 * @author xuefeihu
 *
 */
public class PathClassLoader extends ClassLoader {
	private static final String packageName = "com.moguhu.deep.javaweb.chapter6.section6_6_myclassloader";
	private String classPath;
	
	public PathClassLoader(String classPath) {
		this.classPath = classPath;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		if(packageName.startsWith(name)) {
			byte[] classData = getData(name);
			if(classData == null) {
				throw new ClassNotFoundException();
			} else {
				return defineClass(name, classData, 0, classData.length);
			}
		} else {
			return super.findClass(name);
		}
	}

	private byte[] getData(String className) {
		String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
		try {
			InputStream is = new FileInputStream(path);
			ByteArrayOutputStream stream = new ByteArrayOutputStream();
			byte[] buffer = new byte[2048];
			int num = 0;
			while((num = is.read(buffer)) != -1) {
				stream.write(buffer, 0, num);
			}
			return stream.toByteArray();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// TODO
		}
		return null;
	}
	
}

从上面代码可以看出,classPath目录下的class文件使用自定义的ClassLoader加载,其他的类还是使用父类加载器去加载。

继承URLClassLoader

URLClassLoader可以设定自定义的URL来加载Class文件,如下所示:

public class URLPathClassLoader extends URLClassLoader {
	private String packageName = "com.moguhu.deep.javaweb.chapter6.section6_6_myclassloader";

	public URLPathClassLoader(URL[] urls, ClassLoader parent) {
		super(urls, parent);
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		Class<?> aClass = findLoadedClass(name);
		if(aClass != null) {
			return aClass;
		}
		if(!packageName.startsWith(name)) {
			return super.loadClass(name);
		} else {
			return findClass(name);
		}
	}

}

热替换实现思路

热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。大部分的脚本语言都是天生支持热替换的,如:PHP。而对于Java来说,实现热替换的思路如下所示:

在实现时,首先需要自定义ClassLoader(如下代码所示),他可以在给定的目录下查找目标类,主要的实现思路是重载findClass()方法。

自定义ClassLoader

/**
 * 自定义ClassLoader
 * 
 * @author xuefeihu
 *
 */
public class MyClassLoader extends ClassLoader {
	private String fileName;

	public MyClassLoader(String fileName) {
		this.fileName = fileName;
	}

	@Override
	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.allocate(1024);
				while(true) {
					int i = fileC.read(buffer);
					if(i == 0 || i == -1) {
						break;
					}
					buffer.flip();
					outC.write(buffer);
					buffer.clear();
				}
				fis.close();
				byte[] bytes = baos.toByteArray();
				
				clazz = defineClass(className, bytes, 0, bytes.length);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		return clazz;
	}

	/**
	 * 获取Class文件路径
	 */
	private String getClassFile(String className) {
		// TODO Auto-generated method stub
		return null;
	}
	
}

需要热替换的类

/**
 * 需要热替换的类
 * 
 * @author xuefeihu
 *
 */
public class DemoA {
	
	public void hot(){
		System.out.println("OldDemoA");
	}

}

测试类

public class DoopRun {

	public static void main(String[] args) {
		while(true) {
			try {
				MyClassLoader loader = new MyClassLoader("/Users/xuefeihu");
				Class<?> cls = loader.loadClass("com.moguhu.combat.jvm.chapter10.section10_2_classloader.DemoA");
				Object demo = cls.newInstance();
				Method m = demo.getClass().getMethod("hot", new Class[]{});
				m.invoke(demo, new Object[]{});
				Thread.sleep(10000);
			} catch (Exception e) {
				System.out.println("not find");
				try {
					Thread.sleep(10000);
				} catch (Exception e2) {
				}
			}
		}
		
	}

}

上述代码运行时,会不断的打出“OldDemoA”。当我们更改DemoA的打印内容(如:“NewDemoA”),并且重新编译并替换原先.class文件。程序将会打印“NewDemoA”。

Class对象在JVM中只有一份,理论上可以直接替换,然后更新Java栈中所有对原对象的引用关系。看起来被替换了,但是仍然不可行,因为其违反了JVM的设计原则。对象引用关系只有创建者持有和使用,JVM不可以柑橘对象的引用关系。

造成不能动态替换类对象的关键是:对象的状态被保存了,并且被其他对象引用了。一个简单的方法就是不保存对象的状态,对象创建使用后就被释放掉,当修改后就是新的了。这种思想比较典型的例子就是JSP,其他解释型语言亦是如此。


参考:《深入理解Java虚拟机》、《实战Java虚拟机》、《深入分析Java Web》