JVM-类加载机制

405 阅读8分钟

本文主要介绍一下Java类加载器,主要包括类加载过程和类加载机制两部分。类加载过程包括:加载,验证,准备,解析,初始化5个部分,类加载机制包括:类加载器和双亲委派机制。

Java类型

我们知道Java里的类型分为两大类,基本类型和引用类型。Java基本类型有:byte, short, int, long, float, double, boolean, char,都是有Java虚拟机预先定义好的。而引用类型又可细分为:类,接口,数组和范型,由于泛型参数会在编译过程中被擦除,因此Java的引用类型实际上只有前面3种。

Java类加载过程

一个类在 JVM 里的生命周期有 7 个阶段,分别是加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)。

其中前五个部分(加载,验证,准备,解析,初始化)统称为类加载,下面我们就分别来说一下这五个过程。

加载

加载,是指查找字节流,将类的二进制字节码加载到内存中,以便创建类的对象或者执行类的方法。

字节流:最常见的形式就是java代码编译后形成的class文件,除此之外,我们也可以在程序内部直接生成,或者从网络中获取(例如网页中内嵌的小程序 Java applet)字节流。这些不同形式的字节流,都会被加载到 Java 虚拟机中,成为类或接口。

如果找不到二进制字节码,则会抛出NoClassDefFoundError错误。

链接

链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。

验证

验证是链接过程中的第一个阶段,验证所加载的字节码格式是否符合JVM规范。通常而言,Java 编译器生成的class文件必然满足JVM的规范,但由于java类加载机制的灵活性,字节流的来源不一定是class文件,也有可能是网络中获取的或者是程序代码(AOP)生成的,导致来源不可控,被加载的字节码都有可能被恶意篡改。所以为了安全起见,在类加载器中加了验证字节码合法性的过程。

准备

准备阶段,JVM会为类的静态变量分配内存,并将其初始化为默认值。

对于被static final修饰的静态变量,JVM会直接为该变量初始化为其指定的值,例如:public static final int i = 10;JVM在准备阶段,就会给i变量赋值为10。

对于仅仅是被static修饰的静态变量,JVM会直接为该变量初始化为其默认值,例如:public static int i = 10;JVM在准备阶段,就会i变量赋值为0;

在类加载过程中,JVM只会处理类的静态变量,类的成员变量会在对象的创建过程进行处理。

解析

解析过程,把字节码的常量池中的符号引用转化为直接引用。

在class文件被加载至Java虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。

举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。

解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

初始化

在初始化阶段,JVM会执行静态变量的初始化代码(赋值,或者静态代码块),以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。

类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况:

  1. 当虚拟机启动时,初始化用户指定的主类;
  2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
  5. 子类的初始化会触发父类的初始化,在 java 中初始化一个类,那么必然先初始化过java.lang.Object类,因为所有的java类都继承自java.lang.Object
  6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  7. 使用反射API对某个类进行反射调用时,初始化这个类;
  8. 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
public class Singleton {
  private Singleton() {}
  private static class LazyHolder {
    static final Singleton INSTANCE = new Singleton();
  }
  public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
  }
}

上述代码是一个非常经典的使用内部类实现的单例模式,只有当调用Singleton.getInstance()方法的时候,程序才会访问LazyHolder.INSTANCE,才会触发LazyHolder的初始化,又由于类的初始化是线程安全的,并且仅被执行一次,因此该程序在多线程环境下也能确保有且仅一个Singleton实例。

Java类加载器

类加载器分类

类的加载都是有类加载器完成的,Java提供了几种不同类型的类加载器:

  • BootstrapClassLoader启动类加载器:

    • 负责加载%JAVA_HOME%/jre/lib/rt.jar包中的类
    • 启动类加载器是由 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。
    • java.lang.String就是由BootstrapClassLoader加载的。
  • ExtensionClassLoader扩展类加载器:

    • 负责加载%JAVA_HOME%/jre/lib/ext目录下所有jar包中的类
    • java.lang.ClassLoader的子类
    • 父类是BootstrapClassLoader
    • Java9以后引入了模块特性,ExtensionClassLoader更名为PlatformClassLoader
  • ApplicationClassLoader应用程序类加载器:

    • 负责加载其他Classpath路径下的类
    • java.lang.ClassLoader的子类
    • 父类是ExtensionClassLoader

双亲委派

如果用户在自己的程序里定义了一个类String,包名为java.lang,那么会出现什么情况?

我本地使用的java版本为11,报错信息:java: package exists in another module: java.base

尽管每个类加载器加载的路径是明确的,但JVM仍然无法通过类的全限定名去获取类加载器是哪一个,例如上述的案例用户自定义了一个类java.lang.Stringrt.jarjava.lang.String是同一个全限定名。在这种情况下,JVM就需要一定的机制去查找和去重,明确到底加载其中哪个类。JVM使用的就是双亲委派机制来解决这一问题。

public class ClassLoaderDemo {

  public static void main(String[] args) {
    ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
    System.out.println(classLoader.getName());
    ClassLoader parent = classLoader.getParent();
    System.out.println(parent.getName());
    ClassLoader parentParent = parent.getParent();
    System.out.println(parentParent == null);
  }
}

/**
* output:
*    app
*    platform
*    true
*/

在某个类加载器接收到某个类的加载请求时,如果这个类加载器之前没有加载过这个类,那么它便委托父类加载器加载这个类,如果父类加载器之前也没有加载过这个类,那么,父类加载器又会委托父类的父类加载器加载这个类,以此类推,继续往上委托。如果在往上委托的过程中,某个类加载器已经加载了这个类,那么类加载过程结束。如果在往上委托的过程中,直到到达最顶层父类加载器,都没有找到已经加载这个类的加载器,那么,虚拟机会再从上往下请求各个加载器在自己所负责的路径下查找并加载这个类,如果某个加载器所负责的类路径中存在这个类,那么,这个类就由这个加载器来负责加载。

这一机制,也能有效防止java核心代码被恶意篡改,例如自定义一个java.lang.String的类。

自定义ClassLoader

自定义ClassLoader实现比较简单,只需要继承ClassLoader类,实现findClass()方法即可,例如

public class FileSystemClassLoader extends ClassLoader {

  private final String rootDir;

  public FileSystemClassLoader(String rootDir) {
    this.rootDir = rootDir;
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    String path = rootDir + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
    byte[] byteCode = null;
    try (InputStream is = new FileInputStream(path)) {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      byte[] buffer = new byte[4096];
      int readSize = 0;
      while ((readSize = is.read(buffer)) != -1) {
        baos.write(buffer, 0, readSize);
      }
      byteCode = baos.toByteArray();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    if (byteCode == null) {
      throw new ClassNotFoundException(String.format("class name %s", name));
    } else {
      return defineClass(name, byteCode, 0, byteCode.length);
    }
  }

  public static void main(String[] args) throws Exception{
    ClassLoader cl = new FileSystemClassLoader("/Users/ss/dev/workspace/learning-notes/programming_languages/java-learning/deep-in-java/jvm/target/classes");
    Class<?> cls = cl.loadClass("com.shawn.study.deep.in.java.jvm.BitAlgo");
    System.out.println(cls.getSimpleName());
  }
}