作为一种即时编译的编程语言,ClassLoader是Java程序运行的基础。虽然,大部分和我一样的攻城狮平时都不需要和ClassLoader打交道,但是相信大家对于ClassNotFoundExecption和
NoClassDefFoundError多多少少有些印象。这两个类都和ClassLoader关系密切。
首先,java虚拟机实现了三个类加载器:
1. 启动(Bootstrap)类加载器: 负责加载<java_runtime_home>/lib下的类库,由本地代码实现,主要用于初始化java虚拟机,属于java虚拟机本地实现的部分。
2. 标准扩展(Extension)类加载器:负责加载<java_runtime_home>/lib/ext下的类库,由java实现,开发者可以使用该加载器。
3. 系统(system)类加载器:负责加载CLASSPATH路径下的类库,通常开发者自己编写的类库也由该加载器负责加载(bin文件夹也在CLASSPATH路径中),可以由ClassLoader.getSystemClassLoader()获取到该加载器的索引。
此外,开发者自己编写的类加载器为自定义类加载器。
第二,双亲委派机制:
除了,启动类加载器,其他三类(标准扩展类加载器,system类加载器以及自定义加载器)都直接或间接继承自java.long.ClassLoader。
以下为ClassLoader的部分实现代码(JDK1.6):
// The parent class loader for delegation private ClassLoader parent; /** * Creates a new class loader using the specified parent class loader for * delegation. * * <p> If there is a security manager, its {@link * SecurityManager#checkCreateClassLoader() * <tt>checkCreateClassLoader</tt>} method is invoked. This may result in * a security exception. </p> * * @param parent * The parent class loader * * @throws SecurityException * If a security manager exists and its * <tt>checkCreateClassLoader</tt> method doesn't allow creation * of a new class loader. * * @since 1.2 */ protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); } /** * Creates a new class loader using the <tt>ClassLoader</tt> returned by * the method {@link #getSystemClassLoader() * <tt>getSystemClassLoader()</tt>} as the parent class loader. * * <p> If there is a security manager, its {@link * SecurityManager#checkCreateClassLoader() * <tt>checkCreateClassLoader</tt>} method is invoked. This may result in * a security exception. </p> * * @throws SecurityException * If a security manager exists and its * <tt>checkCreateClassLoader</tt> method doesn't allow creation * of a new class loader. */ protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); //默认情况下会使用SystemClassLoader作为parent }
/** * Loads the class with the specified <a href="#name">binary name</a>. The * default implementation of this method searches for classes in the * following order: * * <p><ol> * * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded. </p></li> * * <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method * on the parent class loader. If the parent is <tt>null</tt> the class * loader built-in to the virtual machine is used, instead. </p></li> * * <li><p> Invoke the {@link #findClass(String)} method to find the * class. </p></li> * * </ol> * * <p> If the class was found using the above steps, and the * <tt>resolve</tt> flag is true, this method will then invoke the {@link * #resolveClass(Class)} method on the resulting <tt>Class</tt> object. * * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link * #findClass(String)}, rather than this method. </p> * * @param name * The <a href="#name">binary name</a> of the class * * @param resolve * If <tt>true</tt> then resolve the class * * @return The resulting <tt>Class</tt> object * * @throws ClassNotFoundException * If the class could not be found */ protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); //查找已经加载的类,如果已经加载过,则没必要再次加载
//如果移除这个检查,则可能会因重复定义而抛出LinkageError if (c == null) { try { if (parent != null) { //先委托父加载器加载class c = parent.loadClass(name, false); } else { //如果parent == null, 则我们认为父加载器为启动类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); //如果父类加载器未能完成加载,则由子类具体实现 } } if (resolve) { resolveClass(c);// 进行链接 } return c; }
从loadClass函数的实现,我们发现加载器进行加载时,首先会委托父类加载器尝试加载,这个就是双亲委派机制。四种类型的父子关系如下图,从代码上来说,BootStrapClassLoader并不是ExtensionClassLoader的parent(实际上BootStrapClassLoader为本地语言实现,所以 ExtensionClassLoader的parent=null),但是从逻辑上来说,parent=null等效于parent= BootStrapClassLoader。
以下是四类类加载器的父子关系示意图:
所以,我们可以通过ClassLoader.getSystemClassLoader().getParent()来获得ExtensionClassLoader的索引。
值得一提的是,双亲委派机制能较好的满足大部分java应用的需求。但是,在一些特殊场景下,也有为了提升性能而修改双亲委派机制的情况,例如:先由当前加载器加载类,加载失败再委托父类加载器加载。
第三,每一个ClassLoader都对应一个命名空间,而jvm通过这个命名空间+class的全名(包括package name)唯一的标识一个类。所以,有几个问题需要注意:
1. 同一个类其实是可以重复加载的,如果你使用两个不同的类加载器来load同一个class,则可以在jvm存在两个完全相同的类定义(它们的命名空间不同)。
2. 由第一问题,引申出来的问题,同一个类经由不同的类加载器加载,对于JVM来说就是不同的类,所以,可能发生ClassCastException,例如:
ClassA a = new ClassA(); // ClassA经由ClassLoader1加载
ClassA b = null; // ClassA经由ClassLoader2加载
b = (ClassA)a; //抛出ClassCastException
3. 因为双亲委派机制的存在,所以,执行ClassLoaderA.loadClass("com.example.classA")加载的classA并不一定是由ClassLoaderA加载的,也有可能是由其父加载器加载的,例如(SystemClassLoader)。这种情况下,ClassLoaderA被成为classA的初始类加载器,而SystemClassLoader为定义类加载器,
类的命名空间由定义类加载器。
第四,java动态加载
当项目存在一些特殊需求,例如:
1. app运行时需要从网络获取最近的class/jar文件,动态更新
2. app有较高的安全需求,对class/jar文件做了额外的加密操作,使通常的类加载器无法解析class/java文件
等特殊情况,就需要考虑动态加载.
常用的java动态加载方案有Class.forName和自定义类加载器。
利用Class.forName实现动态加载的常见案例是JDBC驱动的加载。
Class.forName函数有两种重载:
public static Class<?> forName(String className)
throws ClassNotFoundException {
return forName0(className, true, ClassLoader.getCallerClassLoader());//默认情况下调用Class.forName函数的调用者的ClassLoader进行加载(classloader参数=ClassLoader.getCallerClassLoader()),并完成连接和初始化(initialize参数=true)。
}
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
if (loader == null) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader ccl = ClassLoader.getCallerClassLoader();
if (ccl != null) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader);
}
使用自定义类加载器实现动态加载的常见案例是,利用实现代码的动态更新(或许这个特性未来会在云OS这种特定平台上大展身手)。
其实理论上来说,标准扩展类加载器和系统类加载器也可以实现动态加载,但是,一般来说,这两个加载器内类扫描路径(<java-runtime-home>/lib/ext,以及CLASSPATH)下的文件不会变动,所以一般都以静态方式使用。
根据sun的建议实现一个自定义类加载器还是比较简单的。 继承ClassLoader,并覆盖findClass函数(在sun的标准实现中,标准扩展类加载器和系统类加载器都继承自URLClassLoader,它是ClassLoader的一个子孙类,所以,个人觉得自定义类加载器继承自URLClassLoader也是各不错的选择):
以下代码摘录自:
http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
ClassLoader中比较值得注意的函数有:
1. loadClass函数,此函数实现了已加载类的检查和双亲委派机制的实现。没有不要的情况下,不要覆盖这个函数。
2. findClass函数,此函数由loadClass函数调用,负责寻找需要加载的类对应的字节码(.class文件的内容),如果找不到对应的字节码,则抛出ClassNotFoundException。
3. defineClass,此函数由findClass函数调用,负责解析字节码进而生成对应的类。如果解析失败,则抛NoClassDefFoundError。不建议覆盖。
java动态加载虽然带来了优势,可以让开发者实现很多功能,但是,也存在一定的副作用,因为app的局部是动态变化的,那么静态的部分就无法使用通常的方式来调用动态更新的类。所以,一般需要通过如下两种方式来调用:
1. 接口,动态加载的类始终实现指定的接口,静态部分通过接口来调用动态加载的类。
2. 反射(Reflect),通过指定的类名、函数名(成员名)来调用动态加载的类。
第五,线程上下文类加载器
线程上下文下载器可以由Thread.setContextClassLoader函数和Thread.getContextClassLoader函数设置和获取。线程上下文类加载器可以是任何继承自ClassLoader的加载器实例,可以
是系统类加载器,也可以是自定义类加载器。默认情况下,线程会继承其父线程的ContextClasLoader,而java初始线程的ContextClassLoader为系统类加载器,所以,在未设定的情况下,所有的线程上下文类加载器为系统类加载器。
java为了提高开放性,提供了很多服务提供者接口
(Service Provider Interface,SPI)。而双亲委派机制在这些情形下出现了问题。以JAXP(java xml 解析API)为例:
javax.xml.parsers(JAXP的SPI接口)定义由java核心库提供,由启动类加载器负责加载,而对应的实现 Apache xercers则存在于ClassPath路径下,由系统类加载器负责加载。根据class.forName函数的默认实现规则,java.xml.parsers包内的类会使用启动类加载器去加载Apache的实现类,但是启动类加载器为系统类加载器的父类加载器,启动类加载器不会,也无法调用系统加载器,所以,会导致加载失败。
为了解决这些问题,从java1.2开始引入了上下文加载器,以便SPI接口加载其实现类。
第六,java字节码的格式
ClassLoader内的findClass最终由native代码实现,我一直无缘得见如何将.class文件解析为class,所以去查询了一些其他的资料。
java字节码的格式在《jvm虚拟机规范》中有描述。
ClassFile {
u4 magic; // magic number, 固定为0xCAFEBABE
u2 minor_version; // 子版本号, 一般为0x0
u2 major_version; // 主版本号, 由java的版本号决定,java1.5=0x31, java1.6=0x32, java1.7=0x33
u2 constant_pool_count; //常量池长度
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; // 访问标志,private, package, public...
u2 this_class; // 本类,为常量池内的有效索引
u2 super_class; // 超类,为常量池内的有效索引
u2 interfaces_count; // 实现的接口数量
u2 interfaces[interfaces_count]; // 实现的接口池
u2 fields_count; field_info fields[fields_count]; // 成员数量以及成员池
u2 methods_count; method_info methods[methods_count]; // 方法数量及方法池
u2 attributes_count; attribute_info attributes[attributes_count]; //属性数量及属性池
}
第七, 类加载之后发生的事情
类加载成功之后,jvm会开始执行链接操作(ClassLoader.resolveClass()就是链接一个类)。
链接操作分为如下三个小步骤:
1. 检验:检验操作是为了保证java字节码是正确的。.class文件可能本身不是有效的文件(例如空文件,或者由.mp3
文件修改后缀而来),也有可能是因为java版本不符(java1.5的虚拟机无法解析java1.6的class文件)。如果验证成功则继续链接,否则抛出java.lang.VerifyError错误。
2. 准备:为类的静态变量分配内存空间,初始化默认值。
3. 解析:需要链接的java类一般都会包含对于其他类和接口的引用(包括其父类,实现的接口,方法的参数、返回值所涉及到的类)。解析的目的就是为了保证这些类可以被找到。常用的解析策略包括递归解析和用时解析。递归解析的就是递归加载依赖的接口和类。而更常用的策略则是用时解析,当真正需要使用这个类的时候在进行解析。
链接成功后,jvm接下来会进行初始化。初始化操作主要是执行静态代码块和初始化静态域。
初始化静态域和链接操作中的准备步骤不一样。以如下代码为例:
private static int number = 1;
准备步骤进行的操作,其实在heap上分配4个字节的空间,并在将其初始化为0;
而初始化静态域则是把刚才刚才分配的空间设置为1。
执行静态代码块,这是class文件内的代码第一次被执行。
总结,经过jvm加载,链接,初始化三个步骤的操作,一个class文件转变为java.long.Class的子类,可以在jvm中执行。
参考资料: