JVM 类加载器

文章目录

  • 1 类加载器
    • 1.1 类加载器介绍
    • 1.2 类加载器的加载规则
    • 1.2 类加载器类型总结
  • 2 双亲委派模型
    • 2.1 双亲委派模型介绍
    • 2.2 双亲委派模型的执行流程
    • 2.3 双亲委派模型的好处

回顾一下类加载过程:加载->连接->初始化
其中连接又分为:验证->准备->解析

加载是类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

1 类加载器

1.1 类加载器介绍

根据官方 API 文档的介绍:

类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

从上面的介绍可以看出:

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader。
  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
class Class<T> {
  ...
  private final ClassLoader classLoader;
  @CallerSensitive
  public ClassLoader getClassLoader() {
     //...
  }
  ...
}

简单来说,类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。

1.2 类加载器的加载规则

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoader。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

public abstract class ClassLoader {
  ...
  private final ClassLoader parent;
  // 由这个类加载器加载的类。
  private final Vector<Class<?>> classes = new Vector<>();
  // 由VM调用,用此类加载器记录每个已加载类。
  void addClass(Class<?> c) {
        classes.addElement(c);
   }
  ...
}

1.2 类加载器类型总结

JVM 中内置了三个重要的 ClassLoader

  1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar 、resources.jar 、charsets.jar等 jar 包和类))以及被 -Xbootclasspath参数指定的路径下的所有类。
  2. ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

rt.jar : rt 代表“RunTime”,rt.jar是Java基础类库,包含Java doc里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.* 都在里面,比如java.util.、java.io.、java.nio.、java.lang.、java.sql.、java.math.

JVM 类加载器_第1张图片

除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。

每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoadernull的话,那么该类是通过 BootstrapClassLoader 加载的。( 这是因为BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。)

public abstract class ClassLoader {
  ...
  // 父加载器
  private final ClassLoader parent;
  @CallerSensitive
  public final ClassLoader getParent() {
     //...
  }
  ...
}

2 双亲委派模型

2.1 双亲委派模型介绍

类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。根据官网介绍:

ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。 虚拟机中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。

可以看出:

  • ClassLoader 类使用委托模型来搜索类和资源。
  • 双亲委派模型要求除了顶层的bootstrap classloader外,其余的类加载器都应有自己的父类加载器。
  • ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。

JVM 类加载器_第2张图片
类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。

public abstract class ClassLoader {
  ...
  // 组合
  private final ClassLoader parent;
  protected ClassLoader(ClassLoader parent) {
       this(checkCreateClassLoader(), parent);
  }
  ...
}

组合优于继承,多用组合少用继承。

2.2 双亲委派模型的执行流程

双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoaderloadClass() 中,相关代码如下所示。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}

执行流程如下:

  1. 先去检查该类是否已经被加载过,已经加载的类会被直接返回,否则进行步骤2
  2. 将加载的任务交给父类加载器,如果父类加载器为null,则交给bootstrap classloader, 这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  3. 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。

JVM 判定两个 Java 类是否相同的具体规则 :JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。

2.3 双亲委派模型的好处

  1. 避免重复加载:由于类加载器会在其加载的类中缓存已经加载的类,所以使用双亲委派模型可以避免重复加载同一个类,从而节省内存空间。
  2. 防止类库被篡改:由于父类加载器可以优先加载类库,因此双亲委派模型可以避免用户篡改Java核心类库等安全问题。
  3. 保证类的一致性:如果父类加载器已经加载了一个类,子类加载器再次尝试加载该类时,由于双亲委派模型的存在,子类加载器将不会再次加载该类,而是直接使用父类加载器所加载的类,从而保证类的一致性。
  4. 支持类的版本管理:由于Java类库升级时,只需要升级父类加载器中的类库即可,从而避免类库版本不一致的问题。

你可能感兴趣的:(面试总结,JVM,jvm,java)