[关闭]
@TryLoveCatch 2022-04-17T11:46:50.000000Z 字数 8669 阅读 791

Java知识体系之类加载机制

Java知识体系


类加载

一个java文件,会被编译成class文件,然后jvm把class文件加载到内存,并通过一系列处理,最终转化为可以直接使用的java对象的过程,就是类加载。

什么时候加载类并初始化类呢?

加载类,没有强制的约束,要看交给虚拟机的具体实现。

初始化类呢,主要有这几种情况:

特别的

类加载的过程

加载-连接-初始化-使用-卸载
其中连接阶段包含验证-准备-解析

加载

加载,主要做了三件事:

特别地,第一件事情(通过一个类的全限定名来获取定义此类的二进制字节流)是由类加载器完成的,具体涉及JVM预定义的类加载器、双亲委派模型等内容,后续会介绍。

验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,主要有文件格式验证、元数据验证、字节码验证、符号引用验证等。

验证阶段很重要,但是不是必须的,可以取消。

准备

准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。
这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。

  1. public static int value1 = 123;
  2. public static final int value2 = 123;

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。

准备阶段,类变量已经被赋值过一次初始值(零值)了;初始化阶段,则会根据代码来给类变量在此赋值。
初始化阶段是执行类构造器clinit()方法的过程。

关于类构造器clinit()方法:

类加载器

JVM预定义的三种类加载器:

双亲委托

JVM加载类的时候,采取双亲委托机制,简单来说就是加载任务会先委托给父类加载器。整个流程是这样的:

这里面会有一个问题:

加载器会首先代理给其它类加载器来尝试加载某个类,这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中,一个类用其 全名 和 一个ClassLoader的实例 作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。

Class.forName(String name)默认会使用调用类的类加载器来进行类加载。

双亲委派的作用

1.防止同一个.class文件重复加载
2.对于任意一个类确保在虚拟机中的唯一性.由加载它的类加载器和这个类的全类名一同确立其在Java虚拟机中的唯一性
3.保证.class文件不被篡改,通过委托方式可以保证系统类的加载逻辑不被篡改

主要方法

  1. protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
  2. //1.先检查是否已经加载过--findLoaded
  3. Class<?> c = findLoadedClass(name);
  4. if (c == null) {
  5. try {
  6. //2.如果自己没加载过,存在父类,则委托父类
  7. if (parent != null) {
  8. c = parent.loadClass(name, false);
  9. } else {
  10. c = findBootstrapClassOrNull(name);
  11. }
  12. } catch (ClassNotFoundException e) {
  13. }
  14. if (c == null) {
  15. //3.如果父类也没加载过,则尝试本级classLoader加载
  16. c = findClass(name);
  17. }
  18. }
  19. return c;
  20. }
  1. protected Class<?> findClass(String name) throws ClassNotFoundException {
  2. throw new ClassNotFoundException(name);
  3. }

为什么需要双亲委派?

1.防止同一个.class文件重复加载
2.对于任意一个类确保在虚拟机中的唯一性.由加载它的类加载器和这个类的全类名一同确立其在Java虚拟机中的唯一性
3.保证.class文件不被篡改,通过委托方式可以保证系统类的加载逻辑不被篡改

如何主动破坏双亲委派机制?

因为它的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。

在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

Class.forName(String name)默认会使用调用类的类加载器来进行类加载。

如何自定义一个类加载器?

如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。

文件系统类加载器例子如下:

  1. package classloader;
  2. import java.io.ByteArrayOutputStream;
  3. import java.io.File;
  4. import java.io.FileInputStream;
  5. import java.io.IOException;
  6. import java.io.InputStream;
  7. // 文件系统类加载器
  8. public class FileSystemClassLoader extends ClassLoader {
  9. private String rootDir;
  10. public FileSystemClassLoader(String rootDir) {
  11. this.rootDir = rootDir;
  12. }
  13. // 获取类的字节码
  14. @Override
  15. protected Class<?> findClass(String name) throws ClassNotFoundException {
  16. byte[] classData = getClassData(name); // 获取类的字节数组
  17. if (classData == null) {
  18. throw new ClassNotFoundException();
  19. } else {
  20. return defineClass(name, classData, 0, classData.length);
  21. }
  22. }
  23. private byte[] getClassData(String className) {
  24. // 读取类文件的字节
  25. String path = classNameToPath(className);
  26. try {
  27. InputStream ins = new FileInputStream(path);
  28. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  29. int bufferSize = 4096;
  30. byte[] buffer = new byte[bufferSize];
  31. int bytesNumRead = 0;
  32. // 读取类文件的字节码
  33. while ((bytesNumRead = ins.read(buffer)) != -1) {
  34. baos.write(buffer, 0, bytesNumRead);
  35. }
  36. return baos.toByteArray();
  37. } catch (IOException e) {
  38. e.printStackTrace();
  39. }
  40. return null;
  41. }
  42. private String classNameToPath(String className) {
  43. // 得到类文件的完全路径
  44. return rootDir + File.separatorChar
  45. + className.replace('.', File.separatorChar) + ".class";
  46. }
  47. }

Android中的ClassLoader

Android 中的 ClassLoader

Android 中的 Dalvik/ART 无法像 JVM 那样 直接 加载 class 文件和 jar 文件中的 class,需要通过 dx 工具来优化转换成 Dalvik byte code 才行,只能通过 dex 或者 包含 dex 的jar、apk 文件来加载(注意 odex 文件后缀可能是 .dex 或 .odex,也属于 dex 文件),因此 Android 中的 ClassLoader 工作就交给了 BaseDexClassLoader 来处理。

PathClassLoader

PathClassLoader 在应用启动时创建,从 data/app/… 安装目录下加载 apk 文件。

  1. public PathClassLoader(String dexPath, ClassLoader parent) {
  2. super(dexPath, null, null, parent);
  3. }
  4. public PathClassLoader(String dexPath, String libraryPath,
  5. ClassLoader parent) {
  6. super(dexPath, null, libraryPath, parent);
  7. }

PathClassLoader 其 dexPath 比较受限制,一般是已经安装应用的 apk 文件路径。
在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。

DexClassLoader

对比 PathClassLoader 只能加载已经安装应用的 dex 或 apk 文件,DexClassLoader 则没有此限制,可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。

DexClassLoader 的源码里面只有一个构造方法

  1. public DexClassLoader(String dexPath, String optimizedDirectory,
  2. String libraryPath, ClassLoader parent) {
  3. super(dexPath, new File(optimizedDirectory), libraryPath, parent);
  4. }

BaseDexClassLoader

PathClassLoader 和 DexClassLoader,但这两者都是对 BaseDexClassLoader 的一层简单封装,真正的实现都在 BaseDexClassLoader 内。

  1. @Override
  2. protected Class<?> findClass(String name) throws ClassNotFoundException {
  3. List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
  4. Class c = pathList.findClass(name, suppressedExceptions);
  5. ...
  6. return c;
  7. }
  8. @Override
  9. protected URL findResource(String name) {
  10. return pathList.findResource(name);
  11. }
  12. @Override
  13. protected Enumeration<URL> findResources(String name) {
  14. return pathList.findResources(name);
  15. }
  16. @Override
  17. public String findLibrary(String name) {
  18. return pathList.findLibrary(name);
  19. }

可以看出来,findClass() 、findResource() 均是基于 pathList 来实现的

  1. private final DexPathList pathList

我们来看DexPathList 的 findClass() 方法

  1. public Class findClass(String name, List<Throwable> suppressed) {
  2. // 遍历 dexElements 数组,依次寻找对应的 class,一旦找到就终止遍历
  3. for (Element element : dexElements) {
  4. DexFile dex = element.dexFile;
  5. if (dex != null) {
  6. Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
  7. if (clazz != null) {
  8. return clazz;
  9. }
  10. }
  11. }
  12. // 抛出异常
  13. if (dexElementsSuppressedExceptions != null) {
  14. suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
  15. }
  16. return null;
  17. }

这里有关于热修复实现的一个点,就是将补丁 dex 文件放到 dexElements 数组前面,这样在加载 class 时,优先找到补丁包中的 dex 文件,加载到 class 之后就不再寻找,从而原来的 apk 文件中同名的类就不会再使用,从而达到修复的目的

至此,BaseDexClassLader 寻找 class 的路线就清晰了:

  1. 当传入一个完整的类名,调用 BaseDexClassLader 的 findClass(String name) 方法
  2. BaseDexClassLader 的 findClass 方法会交给 DexPathList 的 findClass(String name, List<Throwable> suppressed 方法处理
  3. 在 DexPathList 方法的内部,会遍历 dexFile ,通过 DexFile 的 dex.loadClassBinaryName(name, definingContext, suppressed) 来完成类的加载

参考

JVM类生命周期概述:加载时机与加载过程
深入理解Java对象的创建过程:类的初始化与实例化
深入理解Java类加载器(一):Java类加载原理解析
一看你就懂,超详细java中的ClassLoader详解
热修复入门:Android 中的 ClassLoader
类加载机制:全盘负责和双亲委托

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注