专栏首页用户7113604的专栏Java ClassLoader加载class过程
原创

Java ClassLoader加载class过程

引言

关于class loader有太多太多的文章和图来讲过程。我就不多说了。以下是我认为的一些要点。

Class load 的基本步骤

   synchronized (getClassLoadingLock(name)) {
               // First, check if the class has already been loaded
               Class<?> c = findLoadedClass(name);
               if (c == null) {
                       if (parent != null) {
                           c = parent.loadClass(name, false);
                       } else {
                           c = findBootstrapClassOrNull(name);
                       }
                   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;
           }

以上是ClassLoader的loadClass方法的摘要。我们可以看到并发和重复加载控制,双亲委派的逻辑(parent.loadClass(name, false))。

findClass后就能拿到Class对象,然后再resolve。findClass的实现都是在子类中,但是都需要经过defineClass

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                            ProtectionDomain protectionDomain)
           throws ClassFormatError
       {
           protectionDomain = preDefineClass(name, protectionDomain);
           String source = defineClassSourceLocation(protectionDomain);
           Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
           postDefineClass(c, protectionDomain);
           return c;
       }
   
   //preDefineClass method
   if (!checkName(name))
       throw new NoClassDefFoundError("IllegalName: " + name);
   
   // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
   // relies on the fact that spoofing is impossible if a class has a name
   // of the form "java.*"
   if ((name != null) && name.startsWith("java.")) {
       throw new SecurityException
           ("Prohibited package name: " +
            name.substring(0, name.lastIndexOf('.')));
   

忽略ProtectionDomain,因为涉及java的安全策略,我也不了解,并且不影响class load的过程理解。

注意一下preDefineClass中的两个if。它对传入的class的全名有一定约束,特别是不能以java.开头。由于defineClass是final的,所以如果你用自定义加载器也没法加载这样的类。

至于defineClass和剩下的resolve都是native方法,并且resolve不是在load的是否必须的。但是在你创建实例的时候JVM是已经对这个class resolve过了。

所以load一个class文件的核心其实是defineClass以及对应native方法。native就包含了对class文件内容的校验等。解析是另一个方法resolveClass。

错误的class loader

以下代码不严谨处麻烦忽略!!!!!!

   public class ClzLoaderSimp extends ClassLoader{
     static String sf = "C:\\Users\\Hello.class";
     @Override
     public Class<?> loadClass(String name){
       try {
         InputStream is = new FileInputStream(new File(sf));
         byte[] clzData = new byte[is.available()];
         is.read(clzData);
         return defineClass(name, clzData, 0, clzData.length);
       } catch (Exception e) {
         e.printStackTrace();
       }
       return null;
     }
   
     public static void main(String[] args) throws ClassNotFoundException{
       ClassLoader loader= new ClzLoaderSimp();
       Class<?> a = loader.loadClass("Hello");
       System.out.println(a.hashCode());
     }
   }

以上class loader运行时输出

   java.lang.SecurityException: Prohibited package name: java.lang
   	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
   ......
   Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
   ......

原因是没有遵循双亲委派。

当我们尝试加载Hello类时,发现父类Object没有加载,所以会先加载父类(这也是class加载过程中的一个点)。在我们的class尝试load父类时没有通过preDefineClass校验抛出了异常。

按理说Object这么基础的类早该已经加载了,为什么又要加载呢?因为JVM内部对一个类的唯一标识是全路径名(包名+类名)+该类的class loader。

由于我们没有遵循双亲委派,并且jvm内部内部已经加载的java/lang/Object的class loader也不是ClzLoaderSimp,所以没有找到对应父类。这个时候就出现了上述的行为和错误。

参考jvm代码,当然这里面的实现细节对java开发者来说没有什么意义(起码我是这么认为的,至今没碰到设计一个类时考虑如何能让jvm 更快加载)

   
       jobject loader = (caller == NULL) ? NULL : get_class_loader(env, caller);
       jobject pd     = (caller == NULL) ? NULL : JVM_GetProtectionDomain(env, caller);
       return Unsafe_DefineClass_impl(env, name, data, offset, length, loader, pd);
   
   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) 

详细过程还是非常冗长的,其中在解析完clss文件的数据后会有一个方法

SystemDictionary::find_or_define_instance_class

    Symbol*  name_h = k->name(); // passed in class_name may be null
     ClassLoaderData* loader_data = class_loader_data(class_loader);
   
     unsigned int d_hash = dictionary()->compute_hash(name_h, loader_data);
     int d_index = dictionary()->hash_to_index(d_hash);
   
   

以及void SystemDictionary::define_instance_class(instanceKlassHandle k, TRAPS)

   Symbol*  name_h = k->name();
     unsigned int d_hash = dictionary()->compute_hash(name_h, loader_data);
     int d_index = dictionary()->hash_to_index(d_hash);
     check_constraints(d_index, d_hash, k, class_loader_h, true, CHECK);
   
     // Register class just loaded with class loader (placed in Vector)
     // Note we do this before updating the dictionary, as this can
     // fail with an OutOfMemoryError (if it does, we will *not* put this
     // class in the dictionary and will not update the class hierarchy).
     // JVMTI FollowReferences needs to find the classes this way.
     if (k->class_loader() != NULL) {
       methodHandle m(THREAD, Universe::loader_addClass_method());
       JavaValue result(T_VOID);
       JavaCallArguments args(class_loader_h);
       args.push_oop(Handle(THREAD, k->java_mirror()));
       JavaCalls::call(&result, m, &args, CHECK);
     }

我们可以看到jvm在存储class数据时候需要class的name(包含包名)以及对应classloader数据来做hash。

在这个过程中其实可以去看看class文件的解析代码,包含了魔数,minor version等在介绍class文件结构时提到的东西。

   ClassFileParser::parseClassFile(Symbol* name,
                                                                  ClassLoaderData* loader_data,
                                                                  Handle protection_domain,
                                                                  KlassHandle host_klass,
                                                                  GrowableArray<Handle>* cp_patches,
                                                                  TempNewSymbol& parsed_name,
                                                                  bool verify,
                                                                  TRAPS)

回到java的class loader,简单的来讲可以改成如下

    public Class<?> loadClass(String name) throws ClassNotFoundException {
       Class<?>  clzInst = null;
       try {
         clzInst =  getParent().loadClass(name);
       }catch (ClassNotFoundException e) {
   
       }
       if(clzInst!=null){
         return clzInst;
       }
       try(InputStream is = new FileInputStream(new File(sf)) ) {
         byte[] clzData = new byte[is.available()];
         is.read(clzData);
         return defineClass(name, clzData, 0, clzData.length);
       } catch (IOException e) {
         e.printStackTrace();
       }
       return null;
     }
   

或者单纯重写findClass,就像URLClassLoader一样

   @Override
     protected Class<?> findClass(String name) {
       Path p = Paths.get("C:\\Users\\",name.replace('.', '/').concat(".class"));
       try(InputStream is = new FileInputStream(p.toFile()) ) {
         byte[] clzData = new byte[is.available()];
         is.read(clzData);
         return defineClass(name, clzData, 0, clzData.length);
       } catch (IOException e) {
         e.printStackTrace();
       }
       return null;
     }
   

一个例子

举个例子,实现相同class name的重复加载。我们有两个zip,其中都有一个BoyCry对Cry接口的实现。

通过不同的class loader 能同时加载两个zip中的Boycry。进而实现很多功能,比如插件等等。

关于这一块有个很好的文章可参考,其中有我这个例子中没有错误 https://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/index.html

   
   public interface Cry {
     void doCry();
   }
   
   public class DiffLoader {
   
     public static Map<String,Class> buildExample() throws MalformedURLException, ClassNotFoundException {
       Map<String,Class> data = new HashMap<>();
       String name = "BoyCry";
       String basePath = "C:\\Users\\MyCode\\";
       Path p1 = Paths.get(basePath,"1","BoyCry1.zip");
       ClassLoader loader1 = new URLClassLoader(new URL[]{p1.toUri().toURL()});
       data.put("1",loader1.loadClass(name));
   
       Path p2 = Paths.get(basePath,"2","BoyCry2.zip");
       ClassLoader loader2 = new URLClassLoader(new URL[]{p2.toUri().toURL()});
       data.put("2",loader2.loadClass(name));
       return data;
     }
     public static void main(String[] args) throws Exception{
       Map<String,Class> data = buildExample();
       Cry cry =null;
       cry = (Cry) data.get("1").newInstance();
       cry.doCry();
       cry = (Cry) data.get("2").newInstance();
       cry.doCry();
     }
   }
   

使用Unsafe绕过preDefineClass的尝试

上文提到没有遵守双亲委派时会jvm会尝试加载java/lang/Object,但是因为preDefineClass的校验而失败。我们尝试通过Unsafe直接调用native的方法是否就可以呢?当然不可以!

请看例子 在运行前我们要自己创建一个Object类,包名是java.lang放到对应路径下(C:\Users\MyCode\)

public class UnsafeClzLoaderSimp extends ClassLoader{
  static String sf = "C:\\Users\\MyCode\\";

  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    Path p = Paths.get(sf,name.replace('.', '/').concat(".class"));
    try(InputStream is = new FileInputStream(p.toFile()) ) {
      byte[] clzData = new byte[is.available()];
      is.read(clzData);
      return getUnSafe().defineClass(name,clzData,0, clzData.length,this,null);
    } catch (IOException | NoSuchFieldException | IllegalAccessException e) {
      e.printStackTrace();
    }
    return null;
  }

  static Unsafe getUnSafe() throws NoSuchFieldException, IllegalAccessException {
    Field field = Unsafe.class.getDeclaredField("theUnsafe");
    field.setAccessible(true);
    return(Unsafe) field.get(null);
  }
  public static void main(String[] args) throws ClassNotFoundException{
    ClassLoader loader= new UnsafeClzLoaderSimp();
    Class<?> a = loader.loadClass("Hello");
    System.out.println(a.hashCode());
  }
}

运行结果

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
	at sun.misc.Unsafe.defineClass(Native Method)
	at UnsafeClzLoaderSimp.loadClass(UnsafeClzLoaderSimp.java:19)
	at sun.misc.Unsafe.defineClass(Native Method)
	at UnsafeClzLoaderSimp.loadClass(UnsafeClzLoaderSimp.java:19)
	at UnsafeClzLoaderSimp.main(UnsafeClzLoaderSimp.java:34)

如果看了上文提到parse file的native代码,再往下看应该就知道原因了

SystemDictionary::resolve_from_stream方法

    if (!HAS_PENDING_EXCEPTION &&
        !class_loader.is_null() &&
        parsed_name != NULL &&
        !strncmp((const char*)parsed_name->bytes(), pkg, strlen(pkg))) {
    // It is illegal to define classes in the "java." package from
    // JVM_DefineClass or jni_DefineClass unless you're the bootclassloader
//省略一堆代码..........................
    const char* fmt = "Prohibited package name: %s";
    size_t len = strlen(fmt) + strlen(name);
    char* message = NEW_RESOURCE_ARRAY(char, len);
    jio_snprintf(message, len, fmt, name);
    Exceptions::_throw_msg(THREAD_AND_LOCATION,
      vmSymbols::java_lang_SecurityException(), message);

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一简单线程同步笔试题分享,欢迎纠错分享更多思路

    有线程:worker1、worker2 ,work1只能累加奇数、work2累加偶数,

    小金狗子
  • synchronized和ReentrantLock的性能比较

    最近写了个例子,比较了一下synchronized和ReentrantLock的性能,分享一下数据和个人观点。

    小金狗子
  • ReentranReadWriteLock源码浅析

    c指的AbstractQueuedSynchronizer的state。SHARED_UNIT为65536。

    小金狗子
  • python测试开发django-21.admin后台表名称和字段显示中文

    admin后台页面表名称(默认会多加一个s)和字段名称是直接显示在后台的,如果我们想设置成中文显示需加verbose_name和verbose_name_plu...

    上海-悠悠
  • go监控方案(2) -- metrics

    Metrics本来是一个Java库, 捕获JVM和应用程序级指标。也就是说可以获得代码级别的数据指标,比如方法调用了多少次之类。

    solate
  • 【CB Insights】中国AI创业公司融资总额全球第一,首次超越美国,深度学习专利是美国6倍

    来源:CB Insights 编译:艾霄葆 【新智元导读】根据CB Insights最新报告,全球人工智能创业公司 2017 年的融资额达到了创纪录的 152 ...

    新智元
  • 自动化部署安装nfs+rsync+sersync+nfs客户端+SMTP

    rsync对nfs服务器的目录做实时备份,使用sersync+rsync,每天定时备份配置文件,本地保存7天,rsync服务器上保存180天。

    张琳兮
  • Webpack系列——Webpack + xxx配合使用

    在webpack中使用Babel通过使用babel-loader即可,babel中的配置可以通过options选项进行配置。 安装:

    用户1515472
  • Hadoop学习笔记—9.Partitioner与自定义Partitioner

      在第四篇博文《初识MapReduce》中,我们认识了MapReduce的八大步凑,其中在Map阶段总共五个步骤,如下图所示:

    Edison Zhou
  • Java架构:一文读懂微服务架构的重构策略

    你很有可能正在处理大型复杂的单体应用程序,每天开发和部署应用程序的经历都很缓慢而且很痛苦。微服务看起来非常适合你的应用程序,但它也更像是一项遥不可及的必杀技。如...

    Java知音

扫码关注云+社区

领取腾讯云代金券