天天看点

Android知识巩固—类加载机制

Java中的类加载机制

类加载器简单来说是用来加载 Java 类到 Java 虚拟机中的。Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。

Java类加载的流程

JVM类加载分为5个过程:加载,链接,准备,解析,初始化,使用,卸载。

  1. 加载: 加载主要是将.class文件(并不一定是.class。可以是ZIP包,网络中获取)中的二进制字节流读入到JVM中。

    (1)通过类的全限定名获取该类的二进制字节流。

    (2)静态存储结构转化为方法区的运行时数据结构。

    (3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

  2. 链接:链接就是将Java类的二进制代码合并到java的运行状态中的过程。

    (1)验证:确保加载的类符合JVM规范与安全。

    (2)准备:为static变量在方法区中分配空间,设置变量的初始值。

    (3)解析:虚拟机将常量池的符号引用转变成直接引用。

  3. 初始化:初始化阶段是执行类构造器()方法。在类构造器方法中,它将由编译器自动收集类中的所有类变量的赋值动作(准备阶段的a正是被赋值a)和静态变量与静态语句块static{}合并。
  4. 使用:正常使用。
  5. 卸载:GC把无用对象从内存中卸载。

Java类加载的时机

一个类真正被加载的时机是在创建对象的时候,才会去执行以上过程,加载类。

主动引用(发生类初始化过程)

  1. new一个对象。
  2. 调用类的静态成员(除了final常量)和静态方法。
  3. 通过反射对类进行调用。
  4. 虚拟机启动,main方法所在类被提前初始化。
  5. 初始化一个类,如果其父类没有初始化,则先初始化父类。

被动引用(不会发生类的初始化)

  1. 当访问一个静态变量时,只有真正声明这个变量的类才会初始化。(子类调用父类的静态变量,只有父类初始化,子类不初始化)。
  2. 通过数组定义类引用,不会触发此类的初始化。
  3. final变量不会触发此类的初始化,因为在编译阶段就存储在常量池中。

类加载器

  1. 启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
  2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
  3. 应用程序类加载器(Application ClassLoader):也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。

除此之外,还有自定义的类加载器,它们之间的层次关系被称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。

源码如下:

  1. 首先检查类是否被加载,没有则调用父类加载器的loadClass()方法。
  2. 若父类加载器为空,则默认使用启动类加载器作为父加载器。
  3. 若父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass() 方法。

缓存 -> 父类加载器 -> 没有父类 -> 启动类加载器 -> 自己的 findClass() 方法

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              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) {
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  c = findClass(name);
                  // 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);
          }
          return c;
      }
  }
           

测试代码:

public class Test {
public static void main(String[] args) {
System.out.println(Test.class.getClassLoader());
System.out.println(Test.class.getClassLoader().getParent());
System.out.println(Test.class.getClassLoader().getParent().getParent());
           

执行结果:

[email protected]
[email protected]
null
           

AppClassLoader 和 ExtClassLoader 由 Java 编写并且都是 java.lang.ClassLoader 的子类,而 BootstarapClassLoader 并非由 Java 实现而是由C++ 实现,所以打印结果为null。

双亲委派模型过程为,当某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

Android中的类加载机制

Android中的虚拟机无论是dvm还是art都只能识别dex文件。因此Java中的ClassLoader在Android中不适用。Android中的java.lang.ClassLoader这个类也不同于Java中的java.lang.ClassLoader。Android中的ClassLoader类型也可分为系统ClassLoader和自定义ClassLoader。

  1. BootClassLoader,Android系统启动时会使用BootClassLoader来预加载常用类,与Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代码实现,而是由Java实现的。BootClassLoader是ClassLoader的一个内部类。
  2. PathClassLoader,全名是dalvik/system.PathClassLoader,可以加载已经安装的Apk,也就是/data/app/package 下的apk文件,也可以加载/vendor/lib, /system/lib下的nativeLibrary。
  3. DexClassLoader,全名是dalvik/system.DexClassLoader,可以加载一个未安装的apk文件。

在Activity中打印当前的ClassLoader:

public class Test2Activity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test2);
        Log.i("Test2Activity", Test2Activity.class.getClassLoader()+"");
        Log.i("Test2Activity", Test2Activity.class.getClassLoader().getParent()+"");
        Log.i("Test2Activity", Test2Activity.class.getClassLoader().getParent().getParent()+"");
    }
}
           
2019-01-20 20:25:33.453 5841-5841/? I/Test2Activity: dalvik.system.PathClassLoader[DexPathList[[zip file "/system/framework/org.apache.http.legacy.boot.jar", zip file "/data/app/com.testingapp-_K7ZDNqjamLgiVLWYpv9cg==/base.apk"],nativeLibraryDirectories=[/data/app/com.testingapp-_K7ZDNqjamLgiVLWYpv9cg==/lib/arm64, /system/lib64]]]
2019-01-20 20:25:33.454 5841-5841/? I/Test2Activity: [email protected]
2019-01-20 20:25:33.454 5841-5841/? I/Test2Activity: null
           

从打印的结果也可以证实:App系统类加载器是PathClassLoader,而BootClassLoader是其parent类加载器

Android知识巩固—类加载机制

Android中的类加载器是BootClassLoader、PathClassLoader、DexClassLoader,其中BootClassLoader是虚拟机加载系统类需要用到的,PathClassLoader是App加载自身dex文件中的类用到的,DexClassLoader可以加载直接或间接包含dex文件的文件,如APK等。

PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,它的一个DexPathList类型的成员变量pathList很重要。DexPathList中有一个Element类型的数组dexElements,这个数组中存放了包含dex文件(对应的是DexFile)的元素。BaseDexClassLoader加载一个类,最后调用的是DexFile的方法进行加载的。

参考文章:

类加载机制系列2——深入理解Android中的类加载器

继续阅读