动态加载类的两种方式
动态加载类的两种方式
Java基础类库给我们提供了两种动态加载类的方式,一种是使用Class的静态方法forName,另一种是使用ClassLoader的loadClass方法。两种加载类的方式的使用如代码清单4-1所示。
代码清单4-1 加载class的两种方式
// 使用ClassLoader.loadClass public static void loadClass1() throws ClassNotFoundException { Class<?> classLoaderTestClass = ClassLoaderMain.class.getClassLoader() .loadClass("com.wujiuye.asmbytecode.book.fourth.ClassLoaderTest"); System.out.println(classLoaderTestClass.getName()); } // 使用Class.forName public static void loadClass2() throws ClassNotFoundException { Class<?> classLoaderTestClass = Class.forName( "com.wujiuye.asmbytecode.book.fourth.ClassLoaderTest"); System.out.println(classLoaderTestClass.getName()); }
Class的静态方法forName加载类与ClassLoader的loadClass方法有所区别,下面我们分别使用这两种方式加载一个含有静态代码块的类ClassLoaderTest,ClassLoaderTest类的代码如下。
public class ClassLoaderTest { static { System.out.println("my name is ClassLoaderTest!"); } }
测试代码见代码清单4-1,使用Class.forName方法加载ClassLoaderTest类的测试输出如图4.1所示。

图4.1 使用Class.forName加载ClassLoaderTest
使用ClassLoader加载ClassLoaderTest类的测试输出如图4.2所示。

图4.2 使用ClassLoader加载ClassLoaderTest
对比图4.1和4.2,很明显的区别就是使用Class静态方法forName加载ClassLoaderTest时,ClassLoaderTest的静态代码块被调用了,而静态代码块是被编译器编译后放入类的初始化方法<clinit>中的,也就是说,使用Class静态方法forName方法加载类会触发该类初始化,而ClassLoader的loadClass方法则不会。
要了解这两个加载类的方式有什么区别,我们可以从虚拟机的源码中寻找答案。以opendjdk1.8[1]为例,在源码根目录下,hotspot目录存放的是hotspot虚拟机的源码,jdk目录存放java开发工具包的源码。其中jdk/src/share目录下的classes目录是存放java基础类库的源码,native目录则是java基础类库中的native方法的实现。源码目录结构如下。
openjdk -- hotspot | jdk -- src -- share -- classes | native
Class的静态方法forName与ClassLoader的loadClass方法最后都会调用一个native方法,想要查找java基础类库中某个native方法的c实现,可到jdk/src/share/native的同包名目录下找到相同文件名的c代码文件,在c代码文件中找到对应的方法。
通过查看java源码我们知道,ClassLoader的loadClass方法最终都会调用到defineClass0、defineClass1、defineClass2这几个native方法其中的一个。在本例中使用只有一个参数的loadClass方法,因此最后调用的是defineClass1方法,对应的c代码如代码清单4-2所示。
代码清单4-2 defineClass1对应的native方法部分源码
JNIEXPORT jclass JNICALL Java_java_lang_ClassLoader_defineClass1(JNIEnv *env, jobject loader,jstring name, jbyteArray data,jint offset,jint length,jobject pd, jstring source) { jbyte *body; char *utfName; jclass result = 0; char* utfSource; ...... body = (jbyte *)malloc(length); ...... result = JVM_DefineClassWithSource(env, utfName, loader, body, length, pd, utfSource); ..... return result; }
这只是截取了其中一部分源代码。无论是defineClass0、defineClass1、defineClass2这几个方法其中的哪一个,最后都会调用虚拟机的对外接口JVM_DefineClassWithSource。
我们可以到hotspot源码中查看该方法的实现,hotspot/src/share/vm/prims目录存放HotSpot虚拟机的对外接口,包括部分标准库的native部分和JVMTI实现。JVM_DefineClassWithSource等native方法中调用的方法可在hotspot源码的prims包下的jvm.cpp文件中找到。
JVM_DefineClassWithSource的源码如代码清单4-3所示。
代码清单4-3 JVM_DefineClassWithSource源码
JVM_ENTRY(jclass, JVM_DefineClassWithSource(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source)) JVMWrapper2("JVM_DefineClassWithSource %s", name); return jvm_define_class_common(env, name, loader, buf, len, pd, source, true, THREAD); JVM_END
该方法并未有多余的动作,而是直接调用了jvm_define_class_common方法完成类的加载。jvm_define_class_common的部分源码如代码清单4-4所示。
代码清单4-4 jvm_define_class_common部分源码
static jclass jvm_define_class_common(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source, jboolean verify, TRAPS) { ...... // 1 assert(THREAD->is_Java_thread(), "must be a JavaThread"); JavaThread* jt = (JavaThread*) THREAD; ...... // 2 TempNewSymbol class_name = NULL; if (name != NULL) { const int str_len = (int)strlen(name); if (str_len > Symbol::max_length()) { THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name); } class_name = SymbolTable::new_symbol(name, str_len, CHECK_NULL); } ........ // 3 Klass* k = SystemDictionary::resolve_from_stream(class_name, class_loader, protection_domain, &st, verify != 0, CHECK_NULL); ....... return (jclass) JNIHandles::make_local(env, k->java_mirror()); }
由于方法比较长,所以只截取了一些步骤,如代码清单4-4中的注释1、2、3所示。注释1是确保加载类的线程是Java线程;注释2是验证类名的长度,如果类名不为空,则类名不能超出最大长度,否则不被允许放入常量池,抛出NoClassDefFoundError;注释3是调用SystemDictionary类的resolve_from_stream方法解析class文件字节流,该方法返回一个Klass指针,这便是hotspot虚拟机将class文件字节流解析完成后生成的c++对象,该对象存储在方法区中。resolve_from_stream方法将在下一节类加载过程继续分析。
现在我们分析Class的静态方法forName的底层实现。该方法有两个重载方法,在不传入类加载器时,默认使用的类加载我们可以通过调用加载后的Class对象的getClassLoader方法拿到,默认是AppClassLoader。forName方法的源码如代码清单4-5所示。
代码清单4-5 Class.forName源码
public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); } private static native Class<?> forName0(String name, boolean initialize, ClassLoader loader,Class<?> caller) throws ClassNotFoundException;
JVM_FindClassFromCaller的源码可在jvm.cpp文件中找到,我们跳过JVM_FindClassFromCaller,因为该方法是通过调用find_class_from_class_loader方法继续完成类加载的。如代码清单4-6所示。
代码清单4-6 find_class_from_class_loader的部分源码
jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init, Handle loader, Handle protection_domain, jboolean throwError, TRAPS) { Klass* klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL); KlassHandle klass_handle(THREAD, klass); // 检查是否应该初始化类 if (init && klass_handle->oop_is_instance()) { klass_handle->initialize(CHECK_NULL); } return (jclass) JNIHandles::make_local(env, klass_handle->java_mirror()); }
从find_class_from_class_loader方法中就可以看出,使用Class的静态方法forName加载类会触发类的初始化方法被调用是因为在类加载完成后调用了类的初始化方法。而类的加载部分是调用SystemDictionary的resolve_or_fail方法完成的,在类没有被加载过的情况下,最后都是通过调用类加载器的loadClass方法完成类的加载。我们不再深入分析,感兴趣的读者可以深入去了解。
注释:
[1] Openjdk1.8下载地址:https://github.com/unofficial-openjdk/openjdk/tree/jdk8u/jdk8u