欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ClassLoader类加载器

程序员文章站 2022-07-03 14:28:11
...

        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);

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

ClassLoader类加载器

        下面介绍一下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类加载器

        在实现时,首先需要自定义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》



链接:http://moguhu.com/article/detail?articleId=49