结合JVM源码浅谈Java类加载器

一、前言

之前文章 Java 类加载器揭秘 从Java层面讲解了Java类加载器的原理,这里我们结合JVM源码在稍微深入讲解下。

二、Java类加载器的委托机制

Java 类加载器使用的是委托机制,也就是一个类加载器在加载一个类时候会首先尝试让父类加载器来加载。那么问题来了,为啥使用这种方式?

使用委托第一这样可以避免重复加载,第二,考虑到安全因素,下面我们看下ClassLoader类的loadClass方法:

protected Class<?> loadClass(Stringname,boolean resolve)  
       throws ClassNotFoundException  
   {  
       synchronized (getClassLoadingLock(name)) {  
           // 首先从jvm缓存查找该类
           Class c = findLoadedClass(name); // (1)
           if (c ==null) {  
               longt0 = System.nanoTime();  
               try {  //然后委托给父类加载器进行加载
                   if (parent !=null) {  
                       c = parent.loadClass(name,false);  (2)
                   } else {  //如果父类加载器为null,则委托给BootStrap加载器加载
                       c = findBootstrapClassOrNull(name);  (3)
                   }  
               } catch (ClassNotFoundExceptione) {  
                   // ClassNotFoundException thrown if class not found  
                   // from the non-null parent class loader  
               }  

               if (c ==null) {  
                   // 若仍然没有找到则调用findclass查找
                   // to find the class.  
                   longt1 = System.nanoTime();  
                   c = findClass(name);  (4)

                   // this is the defining class loader; record the stats  
                   sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 -t0);  
                   sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);  
                   sun.misc.PerfCounter.getFindClasses().increment();  
               }  
           }  
           if (resolve) {  
               resolveClass(c);  //(5)
           }  
           returnc;  
       }  
   }  

代码(1)表示从 JVM 缓存查找该类,如果该类之前被加载过,则直接从 JVM 缓存返回该类。

代码(2)表示如果 JVM 缓存不存在该类,则看当前类加载器是否有父加载器,如果有的话则委托父类加载器进行加载,否者调用(3),委托 BootStrapClassloader 进行加载,如果还是没有找到,则调用当前 Classloader 的 findclass 方法进行查找。

代码(4)则是从本地classloader指定路径进行查找,其中findClass方法在路径找到Class文件会加载二进制字节码到内存,然后后会调用native方法defineClass1解析字节码为JVM内部的kclass对象,然后存放到Java堆的方法区。

代码(5)则是当字节码加载到内存后进行链接操作,对文件格式和字节码验证,并为 static 字段分配空间并初始化,符号引用转为直接引用,访问控制,方法覆盖等,本文对这些不进入深入探讨。

三、JVM源码之defineClass1如何解析字节码文件

本节使用的openjdk7的源码,JVM源码中defineClass1的定义是在ClassLoader.c文件,其解析时序图如下:

 

image.png

可知步骤(8)具体解析字节码文件,步骤(17)添加加载的类到系统词典Map里面,

void SystemDictionary::update_dictionary(int d_index, unsigned int d_hash,
                                         int p_index, unsigned int p_hash,
                                         instanceKlassHandle k,
                                         Handle class_loader,
                                         TRAPS) {
  // Compile_lock prevents systemDictionary updates during compilations
  assert_locked_or_safepoint(Compile_lock);
  Symbol*  name  = k->name();
  ClassLoaderData *loader_data = class_loader_data(class_loader);

  {
  MutexLocker mu1(SystemDictionary_lock, THREAD);

  ...
  // 当前对象已经存在了?
  Klass* sd_check = find_class(d_index, d_hash, name, loader_data);
  //不存在则添加
  if (sd_check == NULL) {
      //添加kclass到系统词典
    dictionary()->add_klass(name, loader_data, k);
    notice_modification();
  }
  ...
}

其中key使用类的包路径+类名,类加载器两者确定,value则为具体加载的类对应的instanceKlassHandle对象,其中维护这kclass对象。也就是系统词典里面使用类加载器和类的包路径类名唯一确定一个类。这也验证了在Java中同一个类使用两个类加载器进行加载后,加载的两个类是不一样的,是不能相互赋值的。

四、JVM源码之findLoadedClass0如何查找一个类是否被加载过了

findLoadedClass0也是在ClassLoader.c文件里面,其查找时序图:

 

image.png

 

如上时序图主要看 SystemDictionary的find方法:

Klass* SystemDictionary::find(Symbol* class_name,
                              Handle class_loader,
                              Handle protection_domain,
                              TRAPS) {

  ...
  class_loader = Handle(THREAD, java_lang_ClassLoader::non_reflection_class_loader(class_loader()));
  ClassLoaderData* loader_data = ClassLoaderData::class_loader_data_or_null(class_loader());

  ...
  unsigned int d_hash = dictionary()->compute_hash(class_name, loader_data);
  int d_index = dictionary()->hash_to_index(d_hash);

  {
    ... 
    return dictionary()->find(d_index, d_hash, class_name, loader_data,
                              protection_domain, THREAD);
  }
}


Klass* Dictionary::find(int index, unsigned int hash, Symbol* name,
                          ClassLoaderData* loader_data, Handle protection_domain, TRAPS) {
  //根据类名和加载器计算对应的kclass在map里面对应的key
  DictionaryEntry* entry = get_entry(index, hash, name, loader_data);
  //存在,并且验证通过则返回
  if (entry != NULL && entry->is_valid_protection_domain(protection_domain)) {
    return entry->klass();
  } else {
     //否者返回null,说明不存在
    return NULL;
  }
}

可知在查找一个类是否已经被加载过后,也是从系统词典里面根据类名和类加载器去查找是否存在的。

五、总结

本文从JVM源码角度分析了Java中唯一含有包路径的类名和类加载器唯一确定了一个类,在全局系统词典里面就是根据包路径的类名和类加载器计算加载的类对应的key的。

最后 高性能 RPC 框架 Dubbo 从入门到深入 已经出炉,深入浅出Dubbo视频欢迎讨论^^

 

image.png

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 结合JVM源码浅谈Java类加载器

  • Trackback 关闭
  • 评论 (1)
    • CodeNongXW
    • 2021/08/19 3:16下午

    受益匪浅,Java类加载器确实很重要,,推荐博主使用一个好用的接口管理工具-ApiPost,免费下载使用,感谢分享

return top