深入理解Java类加载机制

提起热修复以及插件化,相信大家肯定不陌生,而无论是热修复还是插件化,其理论依据就是Android 类加载机制。

类装载的执行过程:

类装载分为以下 5 个步骤:

  • 加载:通过一个类的完整路径查找此类字节码文件(class 文件即二进制文件)。将二进制文件的静态存储结构转化为方法区的运行时数据结构,并利用二进制流文件创建一个Class对象,存储在 Java 堆中用于对方法区的数据结构引用的入口
  • 验证:JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。
  • 准备:JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
  • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程。
    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
  • 初始化:依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。(不包括构造器中的语句)
类加载器

Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中,JVM在加载类的时候,都是通过ClassLoader的loadClass()方法来加载class的,loadClass使用双亲委派模式。

类加载分类

主要有一下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法
    被java程序直接引用。
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。
    Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面
    查找并加载 Java 类。
  3. 系统类加载器(system class loader):它根据 Java 应用的类路径
    (CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来
    完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取
    它。
  4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实
    现。
类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。
即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

ClassLoader

官方给出ClassLoader功能翻译为:
类加载器是负责加载类的对象。ClassLoader类是一个抽象类。给定类的二进制名称,类加载器应尝试查找或生成构成该类定义的数据。一种典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。

ClassLoader从继承关系图:

Android中的ClassLoader主要分为BootClassLoader、PathClassLoader和DexClassLoader这三种类型(BootClassLoader位于ClassLoader同文件下)。

BootClassLoader:主要负责加载Android FrameWork层中的字节码文件; PathClassLoader:负责加载已经安装到系统APK文件中的字节码文件,包括各种第三方库的类,我们自己创建的类(SDK中的类不会被打包到apk中,apk使用的sdk的类是安装到手机中,当前使用手机的Android版本的sdk);
DexClassLoader:负责加载指定目录中的字节码文件

BootClassLoader

BootClassLoader部分代码:

class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;
    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }
    ...
}

BootClassLoader 继承自ClassLoader抽象类,实现方式为单例模式,需要注意的是BootClassLoader的访问修饰符是默认的,只有在同一个包中才可以访问,所以我们在应用程序中是无法直接调用到的。比如系统提供的String、Handler
这样的类是由BootClassLoader来加载的,这样就可以获取这个类的classLoader。

    public static void getClassLoader() {
        System.out.println("String.class的classloader" + String.class.getClassLoader().toString());
        System.out.println("Handler.class的classloader" + Handler.class.getClassLoader().toString());
    }

打印:

String.class的classloaderjava.lang.BootClassLoader@58f82e6
Handler.class的classloaderjava.lang.BootClassLoader@58f82e6

PathClassLoader类代码:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

PathClassLoader继承自BaseDexClassLoader,要在加载类需要传入指定要加载的路径,PathClassLoader构造方法中各个参数的含义:

dexPath:dex文件以及包含dex的apk文件或jar文件的路径集合,多个路径用文件分隔符分隔,默认文件分隔符为‘:’。
librarySearchPath:所使用到的C/C++库存放的路径
parent:该ClassLoader所对应的父ClassLoader

在Activity或者Fragment等组件中直接可以获得classLoader对象,得到的是PathClassLoader,它可以通过context获取到。

public class ContextWrapper extends Context {
    @UnsupportedAppUsage
    Context mBase;

    @Override
    public ClassLoader getClassLoader() {
        return mBase.getClassLoader();
    }
}

        System.out.println("parent classloader" + context.getClassLoader().getParent().toString());
        System.out.println("当前activity的classloader" + context.getClassLoader().toString());

打印:

当前activity的classloaderdalvik.system.PathClassLoader[DexPathList[[dex file "/data/data/com.example.javaprinciples/code_cache/.overlay/base.apk/classes10.dex" ...

parent classloader  java.lang.BootClassLoader@58f82e6

由上述输出日志,我们不仅可以验证,PathClassLoader的parent为BootClassLoader,同时还验证了我们文章开始所说的应用程序的ClassLoader为PathClassLoader,FrameWork层的ClassLoader为BootClassLoader。

DexClassLoader类代码:

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

DexClassLoader 也是继承自BaseDexClassLoader ,相比较PathClassLoader而言,DexClassLoader的构造方法中多了一个参数optimizedDirectory。

optimizedDirectory:Android系统将dex文件进行优化后所生成的ODEX文件的存放路径,该路径必须是一个内部存储路径。PathClassLoader中使用默认路径“/data/dalvik-cache”,而DexClassLoader则需要我们指定ODEX优化文件的存放路径。

上述三种ClassLoader中,PathClassLoader的parent为BootClassLoader,DexClassLoader的parent同样为BootClassLoader。

加载类过程:

首先加载类是通过ClassLoader中的loadClass方法加载,ClassLoader部分代码:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            clazz = findClass(className);
        }

        return clazz;
    }

    protected final Class<?> findLoadedClass(String name) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, name);
    }

loadClass方法中,首先调用findLoadedClass(String)方法检查这个类是否被Jvm加载过,即从缓存中找,findLoadedClass为native方法。如果找到缓存,则直接返回类,如果没有缓存,则去找parent父类加载器去加载类(注意这个类不是父类,持有的父类加载器parent成员变量。如PathClassLoader的parent是BootClassLoader)。如果父类为null,则去层层往上找parent类加载器来加载这个类。如果父加载器不为null,类加载器装载虚拟机内置的加载器调用findClass(String)方法装载类。这个机制叫双亲委派机制。

最终进入到makeDexElements方法:

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * 遍历所有的dex文件
       */
      for (File file : files) {
          if (file.isDirectory()) {    //判断file是否为文件夹
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {          //判断file是否为文件
              //获取文件名称
              String name = file.getName();
              //判断文件名称是否以“.dex”结尾
              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      //将dex文件转换为DexFile对象
                      DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          //创建Element对象,将DexFile对象作为参数传入,
                          //并将该Element对象添加到elements数组中
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  DexFile dex = null;
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

由上述代码,我们可以知道makeDexElements方法的主要作用为:遍历指定路径下的所有文件,将其中的.dex文件转换成DexFile对象,最终存储到elements数组中。DexPathList 的 findClass方法中简单粗暴,对dexElements数组进行遍历,调用element的findClass方法来寻找当前需要的class字节码,简单来讲就是Android在进行类加载的时候,会遍历我们的每一个dex文件,来寻找所需的Class。

  • 什么是双亲委派机制?

某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以加载完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。

双亲委派机制的作用

1.避免重复加载,当父类加载器已经加载了该类的时候,就没有必要子类加载器再加载一次了。

首先,类加载器是按照级别层层往下加载的,当下层的加载器去加载某一个类时,有可能上层的加载已经加载过的,比如FrameWork层的加载被BootClassLoader加载过,下层不用再去加载了。

2.安全性考虑,防止核心API库被随意篡改。
系统类加载器已经加载过了FrameWork层了类,如果我们自己再写一个系统级别的类,创建包java.lang,创建一个自己的String类,类加载器去加载这个类覆盖了原本的java.lang下的String,那么这个时候使用String整个应用就出问题了。

package java.lang;

class String {

    @NonNull
    @Override
    public String toString() {
        return "";
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        return false;
    }
}

而因为双亲委派机制,加载这个String类之前,调用parent的BootClassloader,判断已经加载过String类,这个类其实不会再去找了,解决了被篡改的问题。

类加载的动态性体现:
一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现。

自定义ClassLoader

自定义步骤:
1.编写一个类继承自ClassLoader抽象类。
2.复写它的findClass()方法,在findClass()方法中读取文件流为字节数组。
3.在findClass()方法中调用defineClass()将字节数组转成Class。

public class MyClassLoader extends java.lang.ClassLoader {

    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //加载指定目录下的class文件

        String clsDir = classPath;
        byte[] classData = getClassData(clsDir);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0 ,classData.length);
        }
    }

    /**
     * 从文件读取字节流
     * @param path
     * @return
     */
    private byte[] getClassData(String path) {
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int buffers = 0;
            while((buffers = ins.read(buffer)) != -1) {
                baos.write(buffer, 0 ,buffers);
            }
            return baos.toByteArray();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

Test类代码:

public class Test {

    public static void showClassLoader() {
        System.out.println("Test类已成功加载运行!");
        java.lang.ClassLoader classLoader = Test.class.getClassLoader();
        System.out.println("加载我的classLoader:" + classLoader);
        System.out.println("classLoader.parent:" + classLoader.getParent());
    }
}

加载文件下的class文件,执行类的方法:

        val testClass = MyClassLoader("/User/desktop/Test.class").loadClass("Test")
        testClass.getMethod("showClassLoader").invoke(null)
  • JVM 在搜索类的时候,又是如何判定两个 class 是相同的呢?

JVM 在判定两个 class 是否相同时,不仅要判断两个类名否相同,而且要判断是
否由同一个类加载器实例加载的。
只有两者同时满足的情况下,JVM 才认为这两个 class 是相同的。就算两个 class
是同一份 class 字节码,如果被两个不同的 ClassLoader 实例所加载,JVM 也会
认为它们是两个不同 class。

参考
Android ClassLoader源码解析
ClassLoader源码解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。