深入分析ClassLocader工作机制

文章目录

    • 一、ClassLoader简介
      • 1. 概念
      • 2. ClassLoader类结构分析
    • 二、ClassLoader的双亲委派机制
    • 三、Class文件的加载流程
      • 1. 简介
      • 2. 加载字节码到内存
      • 3. 验证与解析
      • 4. 初始化Class对象
    • 四、常见加载类错误分析
      • 1. ClassNotFoundException
      • 2. NoClassDefFoundError
      • 3. UnsatisfiledLinkError
      • 4. ClassCastException
      • 5. ExceptionInInitializerError
    • 五、自定义ClassLoader的优势

一、ClassLoader简介

1. 概念

ClassLoader顾名思义就是类加载器,负责将Class加载到JVM中。事实上,ClassLoader除了能够将Class加载到JVM意外以外,还有一个重要的作用就是审查每个类应该由谁加载,它是一种父优先的等级加载机制。此外,ClassLoader除了上述的两个作用外还有一个任务就是将Class字节码重新解析成JVM统一要求的对象格式。

2. ClassLoader类结构分析

我们用到ClassLoader时常用下面的几个方法,以及它们的重载方法:

public abstract class ClassLoader {
	ClassLoader;
	Class<?> defineClass(byte[],int,int);
	Class<?> findClass(String);
	Class<?> loadClass(String);
	void resolveClass(Class<?>);
}

defineClass方法用来将byte字节流解析成JVM能够识别的Class对象,有了这个方法我们不仅仅可以通过class文件实例化对象,还可以通过其他方式如我们通过网络接收一个类的字节码,拿这个字节码流直接创建类的Class对象形式实例化对象。defineClass通常是和findClass方法一起使用的,我们通过直接覆盖ClassLoader父类的findClass方法来实现类的加载机制,从而取得想要加载类的字节码。然后调用defineClass方法生成类的Class对象,如果你想在类被加载到JVM中时就被链接,那么可以调用另一个resolveClass方法,当然你也可以选择让JVM来解决什么时候才链接到这个类。

如果你不想重新定义加载类的规则,只想在运行时能够加载自己指定的一个类而已,那么你可以用this.getClass().getClassLoader().loadClass("class")调用ClassLoader的loadclass方法可以获取这个类的Class对象,这个loadClass还有重载方法,你统一可以决定在上面时候解析这个类。

二、ClassLoader的双亲委派机制

双亲委派机制(Parent Delegation Mechanism)是Java中的一种类加载机制。在Java中,类加载器负责加载类的字节码并创建对应的Class对象。双亲委派机制是指当一个类加载器收到类加载请求时,它会先将该请求委派给它的父类加载器去尝试加载。只有当父类加载器无法加载该类时,子类加载器才会尝试加载。

这种机制的设计目的是为了保证类的加载是有序的,避免重复加载同一个类。Java中的类加载器形成了一个层次结构,根加载器(Bootstrap ClassLoader)位于最顶层,它负责加载Java核心类库。其他加载器如扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)都有各自的加载范围和职责。通过双亲委派机制,可以确保类在被加载时,先从上层的加载器开始查找,逐级向下,直到找到所需的类或者无法找到为止。

这种机制的好处是可以避免类的重复加载,提高了类加载的效率和安全性。同时,它也为Java提供了一种扩展机制,允许开发人员自定义类加载器,实现特定的加载策略。
深入分析ClassLocader工作机制_第1张图片
其实Bootstrap ClassLoader并不属于JVM的类等级层次,因为BootStrap ClassLoader并没有遵守ClassLoader的加载规则,另外它并没有子类,ExtClassLoader的父类也不是Bootstrap ClassLoader,我们应用中能取到的顶层父类时ExtClassLoader。

ExtClassLoader和AppClassLoader都位于sun.misc.Launcher类中,它们是Loucher类的内部类。ExtClassLoader和AppClassLoader都继承了URLClassLoader,而URLClassLoader又实现了抽象类ClassLoader,在创建Launcher对象时会首先创建ExtClassLoader,然后将ExtClassLoader作为父加载器创建AppClassLoader对象,而通过Launcher.getClassLoade()方法获取的ClassLoader就是AppClassLoader对象。所以如果Java应用中没有定义其他ClassLoader,那么除了System.getProperty("java.ext.dirs")目录下的类是由ExtClassLoader加载为,其它类都由AppClassLoader来加载。

JVM加载class文件到内存中有两种方式:

  • 隐式加载:所谓隐式加载是不通过在代码里面调用ClassLoader来加载所需要的类,而是铜鼓oJVM来自动加载所需的类到内存的方式。例如:当我们在类中继承或者引用某个类是,JVM在解析当前这个类时发现引用不在内存中,那么自动将这些类加载到内存中。
  • 显式加载:相反的显式加载就是我们在代码中使用ClassLoader类加载一个类的方式

其实这两种方式是混合使用的,例如我们通过自定义的ClassLoader显式加载一个类时,这个类又引用了其他类,那么这些类就是隐式加载的。

三、Class文件的加载流程

1. 简介

下面分析如何将class文件加载到JVM中。ClassLoader加载一个class文件到JVM要经历如下阶段:

深入分析ClassLocader工作机制_第2张图片

  • 首先找到class文件并把这个文件包含的字节码加载到内存中
  • 链接阶段分为三个步骤,分别是字节码验证、Class类数据结构分析及相应的内存分配和最后符号表的链接
  • 最后是类中静态数据和初始化赋值,以及静态块的执行

2. 加载字节码到内存

findClass()的方法是在ClassLoader实现类中实现的,例如URLClassLoader就实现了该方法,URLClassLoader类通过一个URLClassPath类的帮助取得要加载的class文件字节流,而这个URLClassPath定义了到哪里去找这个class文件,如果找到了这个class文件,再读取它的byte字节流通过调用defineClass()方法创建类对象。

 private final URLClassPath ucp;

再看其构造函数,要指定一个URL数据才能创建URLClassLoader对象,也就是必须要指定这个ClassLoader默认到哪个目录中去查找class文件

  public URLClassLoader(URL[] urls, ClassLoader parent) {
        super(parent);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = AccessController.getContext();
        ucp = new URLClassPath(urls, acc);
    }

在创建URLClassLoader对象时就根据传过来的URL数组中的路径来判断是文件还是jar包,根据路径不同分别创建FileLoader或者JarLoader,或者使用默认的加载器,当JVM调用findClass时由这几个加载器来将class文件加载到内存中。

3. 验证与解析

  • 字节码验证,类装入器对于类的字节码要做许多检测,以确保格式正确、行为争取
  • 类准备,这个阶段准备代表的每个类中定义的字段、方法和实现接口所必需的数据结构
  • 解析,在这个阶段类装入器装入类所引用的其他类。可以用许多方式引用类,如超类、结构、字段、方法签名、方法中使用的本地变量

4. 初始化Class对象

类中包含的静态初始化器都被执行,在这一阶段末尾静态字段被初始化默认值。

四、常见加载类错误分析

在执行Java程序时经常会碰到ClassNotFoundExceptionNoClassDefFoundError两个异常,它们都与类加载有关,下面分析一下产生这些异常的原因:

1. ClassNotFoundException

这个异常通常发生在显示加载类的时候,例如,用如下方式调用加载一个类时就报了这个错:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("Jack");
    }
}

深入分析ClassLocader工作机制_第3张图片
出现这个错误的原因是,JVM要加载指定的文件的字节码到内存时,并没有找到这个文件对应的字节码,也就是这个文件并不存在(在当前classpath目录下)。

获取classpath路径的方法:this.getClass().getClassLoader().getResource("").toString()

2. NoClassDefFoundError

这个异常在第一次使用命令执行Java类时很可能会碰到,出现这种异常的可能原因是使用new关键字、属性引用某个类、继承了某个接口或类,以及方法的某个参数引用类某个类,这时会触发JVM的隐时加载这些类时发现这些类不存在。解决这个错误的方法就是确保每个类的引用的类都在当前的classpath下面。

3. UnsatisfiledLinkError

这个异常通常是JVM启动时,如果一不小心将JVM中的某个lib删除了,就可能会报这个错误。

public class Main {
    public native void nativeMethod();
    static {
        System.loadLibrary("Nolib");

    }
    public static void main(String[] args) throws ClassNotFoundException {
        new Main().nativeMethod();

    }
}

深入分析ClassLocader工作机制_第4张图片
上面就是在解析native标识的方法时JVM找不到对应的本机库文件出现。

4. ClassCastException

这个错误比较常见,通常在程序中出现强制类型转换时出现这个错误。JVM在做类型转换时会按照如下规则进行检查:

  • 对于普通对象,对象必须是目标类的实例或目标类的子类实例。如果目标类是一个接口,那么会把它当作实现该接口的一个字类。
  • 对于数组类型,目标类必须是数组类型或java.lang.Objecgt、java.lang.CLoneable、java.io.Serializable

如果不满足上面规则,JVM就会报这个错误。要避免这个错误有两种方式:

  • 在容器类型中显示地指明这个容器所包含的对象类型
  • 先通过instanceof检查是不是目标类型,然后再进行强制类型转换

5. ExceptionInInitializerError

这个错误JVM规范中是这样定义的:

  • 如果Java虚拟机试图创建类ExceptionInInitializerError的新实例,但是因为出现Out-Of-Memory-Error而无法创建新实例,那么就会抛出OutOfMemoryError对象作为代替
  • 如果初始化器抛出一些Exception,而且Exception类不是Error或者它的某个子类,那么就会创建ExceptionInInitializerError类的一个新实例,并用Exception作为参数,用这个实例代替Exception

认值。

五、自定义ClassLoader的优势

通过前面的分析,ClassLoader能够完成的事情无非以下几种情况:

  • 在自定义路径下查找自定义的class文件,也许我们需要的class文件并不总是在已经设置好的ClassPath下面,那么我们必须想办法找到这个类,在这种情况下我们需要自己实现一个ClassLoader
  • 对我们自己的要加载的类做特殊处理,如保证通过网络传输的类的安全性,可以将类经过加密后再传输,在加载到JVM之前需要对类的字节码再解密,这个过程就可以在自定义的ClassLoader中实现
  • 可以定义类的实效机制,如果我们可以检查已经加载的class文件是否修改,如果修改类可以重新加载这个类,从而实现类的热部署。

你可能感兴趣的:(重温Java基础系列,java)