Bootstrap深入精晓Java类加载器

本文重要内容

  • 类加载器基本概念
  • 自定义类加载器
  • 类的隔离
  • Android类加载器案例

虚构机类加载机制
文中早已对类加载机制详细演说了,这两天对类的隔断,破坏双亲委托机制等情节有了新的精通,同时解说下Android上类加载器案例。

以养父母委托机制图镇楼:

类加载器基本概念

顾名思义,类加载器(class loader)用来加载 Java 类到 Java
虚拟机中。类加载器负责读取 Java 字节代码,并转换成
java.lang.Class类的一个实例。

有了Class类实例,就可以透过newInstance方法成立该类的靶子。

一般的话,默认类加载器为当下类的类加载器。比如A类中援引B类,A的类加载器为C,那么B的类加载器也为C。

1、ClassLoader

ClassLoader类是一个抽象类,它定义了类加载器的基本情势。

方法 说明
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。

来看望 loadClass 方法的代码:

protected Class<?> loadClass(String name, boolean resolve){
    Class c = findLoadedClass(name);
    if (c == null) {
        if (parent != null) {
            //使用父加载器加载此类
            c = parent.loadClass(name, false);
        } 
        if (c == null) {
            // 如果父加载器没有成功加载,则自己尝试加载
            c = findClass(name);
        }
    }
    return c;
}

这段代码定义了老人委托模型。所以自定义类加载器尽量不要去重写 loadClass
,而应当重写 findClass 方法。下面让大家来促成一个自定义类加载器。

自定义类加载器

自定义类加载器仍旧很有必不可少的,尤其是在web服务器上,比如tomcat,自定义加载器可以指定自身加载类的限制,甚至由此连续关系,达到类的割裂目标。

Android上也有自定义类加载器,Android上的国策是,每个apk都由不同的类加载器实例来加载。思考一下,假若多个apk中有一致名字的类,倘使由同一个类加载器实例来加载,那一定会搅乱。另一方面也是安全问题。

透过前一章的求学,可知自定义类加载器一般只重写findClass方法即可。真正完成类的加载工作是经过调用
defineClass来兑现的;而启动类的加载过程是透过调用 loadClass来落实的。

  public class FileSystemClassLoader extends ClassLoader { 

   private String rootDir; 
   public FileSystemClassLoader(String rootDir) { 
   this.rootDir = rootDir; 
   } 
   protected Class<?> findClass(String name) throws ClassNotFoundException { 
     byte[] classData = getClassData(name); 
     if (classData == null) { 
         throw new ClassNotFoundException(); 
     } 
     else { 
         return defineClass(name, classData, 0, classData.length); 
     } 
   } 
   private byte[] getClassData(String className) { 
     String path = classNameToPath(className); 
     try { 
       InputStream ins = new FileInputStream(path); 
       ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
       int bufferSize = 4096; 
       byte[] buffer = new byte[bufferSize]; 
       int bytesNumRead = 0; 
       while ((bytesNumRead = ins.read(buffer)) != -1) { 
           baos.write(buffer, 0, bytesNumRead); 
       } 
       return baos.toByteArray(); 
     } catch (IOException e) { 
       e.printStackTrace(); 
     } 
     return null; 
   } 

   private String classNameToPath(String className) { 
     return rootDir + File.separatorChar 
           + className.replace('.', File.separatorChar) + ".class"; 
   } 
}

类的割裂

类的隔断首要有以下两点:

  • 不等的类加载器为同样名称的类成立了附加的称谓空间,不同类加载器加载的类是不配合的。
  • 为了安全或任何目标,使某个模块不能够加载某个类。

第1点相比较简单,网上有为数不少那地方的小说,在此不再讲演。

第2点稍等复杂点,以tomcat为例,tomcat服务器所用到的jar包不希望web应用使用,于是tomcat设计了以下经典的类加载模型:

其中webApp应用的类加载器为 WebApp类加载器,而tomcat服务器类加载器为
Catalina类加载器,如若webApp想加载tomcat所运用的jar包,它先会委托它的父加载器去加载,按照上图所示,它的父加载器也不可能加载(因为tomcat所引述jar包全由Catalina加载,而Catalina并不是WebApp的父加载器),由于它和谐也无能为力加载,所以实现隔离。

在父母委托机制下,同级的类加载器,可以实现类的隔离。

其余,顶层类加载器限制较大,有时它不能加载类也不可能委托子类加载器去加载,也会招致隔离。

1、线程上下文类加载器

JDBC是Java开发者平时碰到的情节,它的主导类为java.sql.DriverManager,查看它的包名,就了然此类是由
启动类加载器(Bootstrap
ClassLoader)加载,但JDBC的求实驱动实现是由逐一厂商自己实现的。DriverManager需要调用由厂商自己实现的接口,那个接口是由
用户程序类加载器(Application ClassLoader)加载。

由前文知,DriverManager由Bootstrap加载,当DriverManager引用厂商实现的JDBC接口时,DriverManager仍然会选拔自己的类加载器,也就是Bootstrap去加载,但Bootstrap只好加载JAVA_HOME下的class文件,厂商实现的JDBC接口,Bootstrap无法加载。这种问题,双亲委托模型已经力不从心化解了。

为了缓解此题材,Java开了后门,也就是添加了线程上下文类加载器,Java为每个线程设置了默认的线程上下文类加载器:Application
ClassLoader,当出现上述情况时,直接动用线程上下文类加载器加载。

  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);

在JDBC的事例中,DriverManager不再使用自己的类加载器(Bootstrap)去加载,而是接纳线程上下文类加载器去加载,而线程上下文类加载器就是
用户程序类加载器(Application ClassLoader),这当然能加载类成功。

这种父类加载器请求子类加载器去加载类的表现,实质上一度损坏了大人委托模型。

2、Class.forName

JDBC在动用此前,一定要调用一句话,Class.forName,很四个人告知我,这是要去加载驱动类。

  Class.forName("com.mysql.jdbc.Driver");

有心人想一想,这不对,假诺代码引用了某个类,会去自动加载类,不需要用户手动加载,除非是眼前的类加载器无法加载此类。

查阅Driver类的源码:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  //
  // Register ourselves with the DriverManager
  //
  static {
      try {
          java.sql.DriverManager.registerDriver(new Driver());
      } catch (SQLException E) {
          throw new RuntimeException("Can't register driver!");
      }
  }
}

类的加载,会有一个开首化阶段,在起始化阶段会执行类的 clinit
方法,也就是会执行类的静态语句块。

由此上述线索发现,其实调用
Class.forName并不是要加载驱动类,而是调用驱动类的静态语句块,向DriverManager注册自己而已。

Android类加载器案例

Android apk动态加载研商
文中提到了Context的类加载器与当下apk的类加载器相同,其实那句话是尴尬的,只是Context类重写了getClassLoader方法。

public ClassLoader getClassLoader() {
    return mPackageInfo != null ?
            mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}

并不是Context的类加载器不同,而是通过LoadedApk获取的类加载器不同。

  mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip,
                mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
                libraryPermittedPath, mBaseClassLoader);

LoadedApk的getClassLoader方法中,遵照apk的不二法门等参数,生成了新的ClassLoader,以保险不同的apk对应着不同的ClassLoader。

  PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(
                                                  zip,
                                                  librarySearchPath,
                                                  libraryPermittedPath,
                                                  parent,
                                                  targetSdkVersion,
                                                  isBundled);

比如,在ActivityThread类中,生成新的Activity时,就利用了新生成的Classloader,确保不同apk的Activity是由不同Classloader生成的。

  Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        。。。
    }

newActivity的代码如下:

  public Activity newActivity(ClassLoader cl, String className,
        Intent intent)
        throws InstantiationException, IllegalAccessException,
        ClassNotFoundException {
    return (Activity)cl.loadClass(className).newInstance();
}

其实就是调用ClassLoader
加载具体Activity全类名,然后调用newInstance生成一个新的靶子。

阅读源码往往有不测的拿走,当时怀疑为什么Context的类加载器不平等,一读源码,收获还挺多的,我们遭受疑难多多读源码吧。

相关文章