前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java ClassLoader加载class过程

Java ClassLoader加载class过程

原创
作者头像
小金狗子
修改2020-03-23 14:37:48
9220
修改2020-03-23 14:37:48
举报
文章被收录于专栏:用户7113604的专栏

引言

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

Class load 的基本步骤

代码语言:txt
复制
   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

代码语言:txt
复制
    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

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

代码语言:txt
复制
   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运行时输出

代码语言:txt
复制
   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 更快加载)

代码语言:txt
复制
   
       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

代码语言:txt
复制
    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)

代码语言:txt
复制
   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文件结构时提到的东西。

代码语言:txt
复制
   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,简单的来讲可以改成如下

代码语言:txt
复制
    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一样

代码语言:txt
复制
   @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

代码语言:txt
复制
   
   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\)

代码语言:txt
复制
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());
  }
}

运行结果

代码语言:txt
复制
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方法

代码语言:txt
复制
    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);

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • Class load 的基本步骤
    • 错误的class loader
    • 一个例子
    • 使用Unsafe绕过preDefineClass的尝试
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档