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

Comments are closed.