ClassLoader与JVM
一、ClassLoader与HotSpot
对JVM有所了解的人应该知道,类文件的格式是不能违背JVM规范的,而JVM自然会有解析类文件的工具ClassFileParser。
// hotspot/src/share/vm/classfile/classFileParser.hpp
ClassFileParser(ClassFileStream* st) { set_stream(st); }
ClassFileParser由ClassFileStream*构造,其实
// hotspot/src/share/vm/classfile/classFileStream.hpp ClassFileStream(u1* buffer, int length, char* source);
可以看出,ClassFileStream其实相当于一个byte[]
ClassFileParser的职责就是解析(过程中包含验证)这个字节数组,并转化成JVM可识别的klassOop。
何为oop,即ordinary object pointer,而klass,即class。
更直白一点,ClassFileParser负责将byte[]数组转化成对应的类,这似乎已经和java.lang.ClassLoader的defineClass功能非常相近了。
现在问题是,这个byte[]是怎么来呢?
关键在于类文件是在何处以何形式存在,例如,可以在本地的不同路径下,也可以在远程机器上,需要网络传输;可以是单独的.class文件,也可以是.jar压缩包的形式存在。
这些都是不确定的,所以ClassFileParser干脆认死理儿,只处理byte[]数组。其实从职责划分上,也应该这么做。
因此,JVM还需要一个更高级的转换工具ClassLoader。JVM内部就包含这个工具,它可以利用一些辅助工具(例如,从jar中读取对应的类文件)对本地的类文件做解析处理。
该ClassLoader可以做到根据类文件名来加载类,这其实就和java.lang.ClassLoader的loadClass功能非常相近了。
// hotspot/src/share/vm/classfile/classLoader.cpp
ClassFileStream* ClassPathDirEntry::open_stream(const char* name) {
...
jio_snprintf(path, sizeof(path), "%s%s%s", _dir,
os::file_separator(), name)
...
}
可以看到path=_dir + 文件分隔符 + name,得到path后,就是读取文件并存放到byte[]的过程了。
要想path直接可用,得有两个前提:
- _dir事先已经配置好;
- name也应该先经过了处理,例如java.lang.String,肯定会先被转换成java/lang/String.class;
打个比方,假如_dir是/home/admin/java/src,name是com/goldendoc/Test.class,最终path即为/home/admin/java/src/com/goldendoc/Test.class。
我们可以看一下load_classfile的过程:
// 将a/b/C转换成a/b/C.class
stringStream st;
st.print_raw(h_name->as_utf8());
st.print_raw(".class");
char* name = st.as_string();
/* 遍历classpath entry,能够打开name对应的文件,并转换成
ClassFileStream,既然ClassFileStream已经有了,自然就
能够把剩余的工作移交给ClassFileParser了。
*/
ClassFileStream* stream = NULL;
{
ClassPathEntry* e = _first_entry;
while (e != NULL) {
stream = e->open_stream(name);
if (stream != NULL) {
break;
}
e = e->next();
}
}
我们可以看看该ClassLoader的初始化过程:
void ClassLoader::initialize() {
...
// lookup zip library entry points
load_zip_library();
// initialize search path
setup_bootstrap_search_path();
...
}
其中load_zip_library()即加载zip库,从而便于处理jar(可以认为jar是一种特殊的zip压缩);而setup_bootstrap_search_path()即为初始化BootstrapClassLoader的查找路径,其实就是在初始化上文中每个ClassPathEntry的_dir。
相比之下 setup_bootstrap_search_path()更能引起关注,因为它带有bootstrap,与后文java的ClassLoader有关。
setup_bootstrap_search_path()的过程很简单:
- char* sys_class_path = os::strdup(Arguments::get_sysclasspath());先拿到sys_class_path;
- 将其split,然后更新到ClassLoader的ClassPathEntry List当中;
// hotspot/src/share/vm/runtime/arguments.hpp
static char *get_sysclasspath() {
return _sun_boot_class_path->value();
}
static void set_sysclasspath(char *value) {
_sun_boot_class_path->set_value(value);
}
// hotspot/src/share/vm/runtime/os.cpp
bool os::set_boot_path(char fileSep, char pathSep) {
const char* home = Arguments::get_java_home();
int home_len = (int)strlen(home);
...
static const char classpath_format[] =
"%/lib/resources.jar:"
"%/lib/rt.jar:"
"%/lib/sunrsasign.jar:"
"%/lib/jsse.jar:"
"%/lib/jce.jar:"
"%/lib/charsets.jar:"
"%/classes";
char* sysclasspath = format_boot_path(classpath_format,
home, home_len, fileSep, pathSep);
if (sysclasspath == NULL) return false;
Arguments::set_sysclasspath(sysclasspath);
return true;
}
fileSep和pathSep分别是文件分隔符和路径分隔符,如在linux下:
// hotspot/src/os/linux/vm/os_linux.cpp
set_boot_path('/', ':')
而windows下:
// hotspot/src/os/linux/vm/os_linux.cpp
set_boot_path('\\', ';')
至于format_boot_path,就是将路径格式化成对应os可认知的路径,’%'被替换成java_home路径,’/'和’:'也相应被替换。
讲了这么多,我们应该能明白,这个ClassLoader的查找路径基本被固化,到现在,我们基本可以认定为它承担着传说中BootstrapClassLoader的部分角色(当然,这不是全部,毕竟java中ClassLoader的一些功能,如defineClass,直接是通过ClassFileParser完成的,而且还不包含JVM的中间处理过程)。
从上文类加载的过程中可以看出两个可扩展的或继承的点,一个就是ClassFileParser能够把byte[]转化成类;另一个就是ClassLoader的类查找路径。
二、 ClassLoader Architecture
关于ClassLoader体系的文章,网上资料非常多,这里就简单描述一下,JVM发展到今天,形成了如下的的classloader architecture:
- Bootstrap ClassLoader/启动类加载器
主要负责jdk_home/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作。
- Extension ClassLoader/扩展类加载器
主要负责jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包装入工作。
- System ClassLoader/系统类加载器
主要负责java -classpath/-Djava.class.path所指的目录下的类与jar包装入工作。
- User Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)
在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性。
每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类。
为了实现java安全沙箱模型顶层的类加载器安全机制, java默认采用了 ” 双亲委派的加载链 ” 结构。
sun.misc.Lanucher的构造过程中,对扩展类加载器和系统类加载器做了初始化,并形成了上述的加载链。
// sun.misc.Launcher.java
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader");
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader");
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
}
三、 双亲委派过程
ClassLoader体系发展成这样是不是必然不可确定,可以确定的是这种委派的方式的确非常巧妙,它的核心逻辑就在java.lang.ClassLoader的loadClass中。
// 查找类是否被装载过
Class c = findLoadedClass(name);
if (c == null) {
// 如果没有被装载过
try {
if (parent != null) {
// 父类加载器不为空,则委派给父类加载
c = parent.loadClass(name, false);
} else {
// 父类加载器为空,则委派给启动类加载器加载
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);
}
}
现在分别解析各个过程:
3.1 findLoadedClass
首先是findLoadedClass,其实它最终调用JVM_FindLoadedClass
// hotspot/src/share/vm/prims/jvm.cppklassOop
k = SystemDictionary::find_instance_or_array_klass(klass_name, h_loader,
Handle(), CHECK_NULL);
// hotspot/src/share/vm/classfile/systemDictionary.cpp
dictionary()->find(d_index, d_hash, class_name,
class_loader,protection_domain, THREAD);
JVM会将加载过的类管理起来,存放到类似hashmap的结构中,有这么几个概念:
- dictionary hashmap桶链式结构,存放已经被加载的类
- placeholders hashmap桶链式结构,存在正在加载中的类
- class_name, classloader 类名和类加载器,各自都有其hash值
- protection_domain 保护域
可以比较形象的来说明这几个概念:
- dictionary 比作一个拥有很多房间的酒店
- placeholders 比作酒店前台的等候队列
- class_name, classloader 对应的hash值联合起来相当于房间门牌号码
- protection_domain 房间钥匙
因此,查看一个类是否被对应的classloader加载,只需要将两者hash值联合起来,相当于拿到一个门牌号码,在dictionary中,找到对应房间,如果能够打开,自然相当于加载过。这个过程不复杂,A类如果被ClassLoader B加载过,则dictionary会将其存放在A, B hash值对应的桶中。
3.2 findBootstrapClassOrNull
再来看findBootstrapClassOrNull的过程,当某classloader对应的父亲为空时,就会调用它来完成类的加载。
// jdk/src/share/native/java/lang/ClassLoader.c
JNIEXPORT jclass JNICALL Java_java_lang_ClassLoader_findBootstrapClass(
JNIEnv *env, jobject loader,
jstring classname)
...
cls = JVM_FindClassFromBootLoader(env, clname);
...
// hotspot/src/share/vm/prims/jvm.cpp
klassOop k = SystemDictionary::resolve_or_null(h_name, CHECK_NULL);
// hotspot/src/share/vm/classfile/systemDictionary.cpp
klassOop SystemDictionary::resolve_or_null(Symbol* class_name, TRAPS) {
// 即null的classloader和不需要protection_domain
return resolve_or_null(class_name, Handle(), Handle(), THREAD);
}
klassOop SystemDictionary::resolve_or_null(Symbol* class_name,
Handle class_loader,
Handle protection_domain,
TRAPS) {
if (FieldType::is_array(class_name)) {
return resolve_array_class_or_null(class_name,
class_loader, protection_domain, CHECK_NULL); }
else if (FieldType::is_obj(class_name)) {
ResourceMark rm(THREAD);
// Ignore wrapping L and ;.
TempNewSymbol name = SymbolTable::new_symbol(class_name->as_C_string()
+ 1, class_name->utf8_length() - 2,
CHECK_NULL);
return resolve_instance_class_or_null(name, class_loader,
protection_domain, CHECK_NULL);
} else {
return resolve_instance_class_or_null(class_name, class_loader,
protection_domain, CHECK_NULL);
}
}
可以看到,resolve_or_null可以处理三种情况下的类名。
resolve_instance_class_or_null的整个过程是比较复杂的,它不一定真的会加载,因为对应的类可能已经被加载,处于dictionary中,或者正在被其它的线程加载,处于placeholders中,总之在确实找不到的情况下,才会调用load_instance_class完成加载。
因为BootstrapClassLoader的加载过程,class_loader肯定为空,所以load_instance_class过程中排除无关逻辑,最终如下:
instanceKlassHandle k;
{
k = load_shared_class(class_name, class_loader, THREAD);
}
if (k.is_null()) {
// Use VM class loader
k = ClassLoader::load_classfile(class_name, CHECK_(nh));
}
先 load_shared_class,尝试从共享类中找到对应的类,如果仍然没有,在会调用load_classfile完成类文件到类的转化。可以看到,bootstrap classloader的加载过程也是非常复杂的,不过最终会采用ClassLoader::load_classfile完成加载。
3.3 findClass
至于findClass,就是需要我们实现的类查找方法,它是个模板方法,双亲委派到最后,仍然找不到我们要加载的类,就可以采用findClass补救,所以,要实现自定义的ClassLoader,最简单的方式就是重写findClass方法。
当然,我们也可以重写loadClass的过程,甚至破坏其双亲委派模式,OSGI的实现上就很好了利用了这一点,这个后文再详细解释。
在重写findClass时,我们很有可能会用到一个final方法,前文也提到过,那就是defineClass方法。
// java.lang.ClassLoader.java
protected final Class defineClass(String name, byte[] b,
int off, int len)
假如我们要自己来完成这个方法,很容就想到了,利用byte[] b构造ClassFileStream并调用ClassFileParser完成类的解析。事实上,jvm就是这么做的,当然,它考虑的更仔细些。
try {
c = defineClass1(name, b, off, len, protectionDomain,
source, verify);
} catch (ClassFormatError cfe) {
c = defineTransformedClass(name, b, off, len, protectionDomain,
cfe, source, verify);
}
其中defineClass1不用看都能知道它肯定用了我说的方法完成类的加载。比较能让我在意的是这个defineTransformedClass,似乎和instrument扯上关系了,猜测在解析出现格式错误时,为采用defineTransformedClass做些转换措施。实际代码验证了我这一点:
Object[] transformers = ClassFileTransformer.getTransformers();
Class c = null;
for (int i = 0; transformers != null && i < transformers.length; i++) {
try {
// Transform byte code using transformer
byte[] tb = ((ClassFileTransformer) transformers[i]).transform(b,
off, len);
c = defineClass1(name, tb, 0, tb.length,
protectionDomain, source, verify);
break;
} catch (ClassFormatError cfe2) {
// If ClassFormatError occurs, try next transformer
}
}
遍历所有的ClassFileTransformer,并transform,再尝试重新加载。关于这部分内容,本章就不准备细讲了。
四、 类的动态加载
关于类显示的动态加载,除了具有代表性的ClassLoader.loadClass,就是Class.forName了。
例如,我们可以ClassLoader.loadClass(“a.b.C”),也可以Class.forName(“a.b.C”)。
它们有什么不同呢?我们从Class.forName的实现出发:
// jdk/src/share/native/java/lang/Class.c
cls = JVM_FindClassFromClassLoader(env, clname, initialize,
loader, JNI_FALSE);
// hotspot/src/share/vm/prims/jvm.cpp
jclass find_class_from_class_loader(JNIEnv* env, Symbol* name,
jboolean init, Handle loader,
Handle protection_domain,
jboolean throwError, TRAPS) {
// Security Note:
// The Java level wrapper will perform the necessary security
// check allowing
// us to pass the NULL as the initiating class loader.
klassOop klass = SystemDictionary::resolve_or_fail(name, loader,
protection_domain, throwError != 0, CHECK_NULL);
KlassHandle klass_handle(THREAD, klass);
// Check if we should initialize the class
if (init && klass_handle->oop_is_instance()) {
klass_handle->initialize(CHECK_NULL);
}
return (jclass) JNIHandles::make_local(env,
klass_handle->java_mirror());
}
// hotspot/src/share/vm/classfile/systemDictionary.cppklassOop
SystemDictionary::resolve_or_fail(Symbol* class_name, Handle class_loader,
Handle protection_domain, bool throw_error, TRAPS) {
klassOop klass = resolve_or_null(class_name, class_loader,
protection_domain, THREAD);
...
return klass;
}
回归到上文中的resolve_or_null和resolve_instance_class_or_null中最终,还是得看load_instance_class的实现,前文也说过一部分,ClassLoader为空的情况,对于ClassLoader不为空的情况
JavaCalls::call_virtual(&result,
class_loader,
spec_klass,
vmSymbols::loadClass_name(),
vmSymbols::string_class_signature(),
string,
CHECK_(nh));
即调用传入的class_loader的loadClass(String name)方法。
因此,假如有ClassLoader a,a.loadClass(“a.b.C”)等效于Class.forName(“a.b.C”, false, a)了。
public static Class forName(String name, boolean initialize,
ClassLoader loader)
所以,ClassLoader.loadClass和Class.forName最主要的区别就在这个initialize,它有什么作用呢?
我们回过头去看Class.forName的JVM实现,有这么一段:
if (init && klass_handle->oop_is_instance()) {
klass_handle->initialize(CHECK_NULL);
}
比较直白,即如果initialize为true,会进行类的初始化工作(注意不是实例化)
五 与ClassLoader有关的常见异常
ClassNotFoundException
– The given class could not be found
NoClassDefFoundError
– A ClassNotFoundException was generated when loading a dependent class
ClassCircularityError
– For example, if loading a superclass calls defineClass() for the original class
ClassFormatError
– Bad bytes in your .class file, e.g. no CAFEBABE
UnsupportedClassVersionError
– Java code compiled using javac from Java 6 but you’re running on Java 5 ?
UnsatisfiedLinkError
– Native library cannot be loaded, or a JNI method is called but the symbol is unknown
VerifyError
– Bytecodes are not valid according to the Java specification