JVM类加载器(一)

程序媛一枚 2020-05-03

Java类加载器

Java虚拟机自带的加载器:

  • 根类加载器(Bootstrap):该加载器没有父加载器,它负责加载虚拟机中的核心类库。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有集成java.lang.ClassLoader类。
  • 扩展类加载器(Extension):它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的jar文件放在这个目录下,也会自动由扩展类加载器加载,扩展类加载器是纯java类,是java.lang.ClassLoader的子类。
  • 系统(应用)类加载器(System):也称为应用类加载器,它的父加载器为扩展类加载器,它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,他是用户自定义的类加载器的默认父加载器。系统类加载器时纯Java类,是java.lang.ClassLoader的子类。

用户自定义加载器:

  • java.lang.ClassLoader的子类
  • 用户可以定制类的加载方式

通过用户自定义类加载器,可以定制类的加载方式,不需要等到某个类被首次主动使用时再加载它。JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了class文件缺失或存在错误,类加载器必须在程序首次主动使用该类才报告错误(LinkageError错误)。如果这个类没有被程序主动使用,那么类加载器也不会报告错误。

类的验证

类被加载后,就进入连接阶段。连接阶段就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

类的准备

在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0。 

public class Sample {
    private static int a = 1;
    public static long b;
    public static long c;

    static {
        b = 2;
    }
}

在程序中,静态变量的初始化有两种途径:

  1. 在静态变量的声明处进行初始化。
  2. 在静态代码块中进行初始化。

在上面的Sample代码中,静态a和b都被显式初始化,而静态变量c没有被显式初始化,它将保持默认值为0。

类的初始化步骤:

  • 假如这个类还没有被加载和连接,那就先进行加载和连接。
  • 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类。
  • 假如类中存在初始化语句(如:静态变量的声明、静态代码块),那就依次执行这些初始化语句。

当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则不适用于接口。因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定的接口的静态变量时,才会导致该接口的初始化。
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用双亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则有父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。

类加载器的父亲委托机制
在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根加载器之外,其余的类加载器都有一个父加载器。这种树形结构并非物理上的树形结构,而是逻辑上的树形结构,如下图:

JVM类加载器(一)

这几个加载器的关系并非继承,而是包含。

JVM类加载器(一)

在上图中,loader1和loader2是我们自定义的加载器,它和Java自带的加载器形成一种父子关系。如果要加载Sample类,加载器会将加载请求一级一级往上抛,如果父加载器能够加载则加载,不能则自己加载。

最顶层的根加载器是无法加载Sample类的,因为根加载器会尝试从特定的目录或指定的路径加载相应的类,如果Sample是我们在编程所编写的类,则根加载器无法加载。根加载器加载失败后,他把这个任务返回给下面的加载器。扩展类加载器尝试加载,但扩展类加载器也是从特定的路径加载,所以加载失败。任务返回给系统加载器,系统加载器可以加载我们工程classpath下所定义的字节码文件,Sample会由系统加载器加载成功,最后告诉loader1加载成功。

JVM类加载器(一)

类加载器的父亲委托机制

  • Bootstrap ClassLoader(启动类加载器):负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class。由C++实现,不是ClassLoader子类。

  • Extension ClassLoader(扩展类加载器):负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。
  • App ClassLoader(系统/应用类加载器):负责记载classpath中指定的jar包及目录中class。

如果有一个类能够成功加载Test类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)称为初始类加载器。比如前面的Sample类,一般是由loader1委托系统类加载器加载成功,因此系统类加载器和loader1也可以被称为初始类加载器。

class类中有一个方法getClassLoader就是用于获取加载这个类的类加载器。如果加载类的加载器为根加载器,则会返回null。比如我们常用的原生类型String、int或者void,在调用getClassLoader都会得到null

我们来用一个例子证实一下:

package com.leolin.jvm;

public class MyTest7 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("java.lang.String");
        //如果加载类的加载器为根加载器,则会返回null
        System.out.println(clazz.getClassLoader());//运行为null,因为是根加载器加载
        Class<?> clazz2 = Class.forName("com.leolin.jvm.C");
        System.out.println(clazz2.getClassLoader());//输出应用类
    }
}

class C {
}

运行结果:

null

sun.misc.Launcher$AppClassLoader代表AppClassLoader是Launcher的内部类,可以看到,String的加载器返回的是null,而C类的加载器是系统类加载器。

我们可以通过ClassLoader.getSystemClassLoader()的方式去获得一个系统类加载器,默认用户自定义类加载器的父加载器就是系统加载器,系统加载器也是用于启动应用的类加载器。

现在,我们尝试用系统类加载器去加载一个我们编写的应用类,看是否会初始化:

package com.leolin.jvm;

class CL {
    static {
        System.out.println("Class CL");
    }
}

public class MyTest12 {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class<?> clazz = loader.loadClass("com.leolin.jvm.CL");
        System.out.println(clazz);
        System.out.println("-------------");
        clazz = Class.forName("com.leolin.jvm.CL");
        System.out.println(clazz);
    }
}

运行结果:

class com.leolin.jvm.CL
-------------
Class CL
class com.leolin.jvm.CL

可以看到,类加载器加载一个类时,并不会导致整个类的初始化。

ClassLoader有个final修饰的成员变量parent,代表委托的父加载器。虚拟机会硬编码这个字段的偏移量,因此所有字段必须添加到这个字段的后面。我们可以获取一个系统加载器,循环打印其父加载器:

package com.leolin.jvm;

public class MyTest13 {
    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        System.out.println(classLoader);
        while (classLoader != null) {
            classLoader = classLoader.getParent();
            System.out.println(classLoader);
        }
    }
}

运行结果如下:

null

果然如我们之前所说,系统类加载器的父加载器是扩展类加载器,而扩展类加载器的父加载器为根加载器,而当我们获取根加载器时,返回null。

Thread.currentThread().getContextClassLoader()返回当前线程的上下文类加载器,当线程中的代码正在加载类或资源时,加载器由线程的创建者提供,如果没有调用

setContextClassLoader 方法设置上下文加载器,默认使用父线程的上下文加载器。原始线程的上下文类加载器通常设置为用于加载应用程序的类加载器。

下面,我们通过当前线程的上下文类加载器去定义一个class文件:

package com.leolin.jvm;


import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;

public class MyTest14 {
    //如何通过给定的字节码路径去把相应的资源信息打印出来
    public static void main(String[] args) throws IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        //接下来去定义一个资源,找到字节码文件全路径用一个/去标识
        String resourceName = "com/leolin/jvm/MyTest13.class";
        //接着我们根据路径去获取字节码文件在什么位置上
        Enumeration<URL> urls = classLoader.getResources(resourceName);
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            System.out.println(url);
        }
    }
}

运行代码,得到如下输出:

file:/D:/F/work/java_space/jvm-lecture/target/classes/com/leolin/jvm/MyTest13.class

获取ClassLoader的途径

  • 获取当前类的ClassLoader:clazz.getClassLoader()
  • 获取当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
  • 获取系统ClassLoader:ClassLoader.getSystemClassLoader()
  • 获取调用者的ClassLoader:DriverManager.getCallerClassLoader()

相关推荐