JVM - 类加载器

# 类加载器及类加载器执行过程

JDK版本:1.8

# 1、类加载器子系统

下图为类加载子系统:

JVM - 类加载器_第1张图片

  • 类加载子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识(CA FE BA BE)。
  • Classloader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
  • 记载的类信息存放在jvm内存中的一块名为Method Area的内存空间中。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)。

# 2、类加载器Classloader的角色

JVM - 类加载器_第2张图片

  • class file源码文件存在于本地硬盘上,这一理解为一个模板。而这个模板在执行的时候是需要加载到JVM内存中根据这个文件实例化出n个一摸一样的实例。加载class file的方式是二进制流。
  • class file源码被加载到JVM中,被称为DNA元数据模板,存放在JVM内的Method Area中。
  • .class文件--> JVM --> 最终成为DNA元数据模板。这个过程需要一个运输工具(类装载器Classloader)完成。

# 3、类加载的过程

Java类加载的过程(宏观):

JVM - 类加载器_第3张图片

# 3.1、加载(Loading)阶段

加载(Loading)阶段分为3步:

  • 1、通过被加载类的全限定名(包名 + 类名)获取定义此类的二进制字节流。
  • 2、将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。

加载.class文件的方式:

  • 从本地系统中直接加载。
  • 通过网络获取,典型场景:Web Applet
  • zip压缩包中读取,成为日后jarwar格式的基础。
  • 运行时计算生成,使用最多的是:动态代理技术。
  • 由其他文件生成,典型场景:JSP应用。
  • 从专有数据库中提取.class文件。
  • 从加密文件中获取,典型的防止Class文件被反编译的保护措施。

# 3.2、链接(Linkging)阶段

链接(Linking)阶段分为3个步骤:

  • 验证(Verification):

    • 1、确保被加载类的Class文件的二进制字节流中包含的信息符合当前虚拟机的要求。保证被加载类的正确性,不会危害虚拟机自身安全。
    • 2、主要包括四种验证方式:文件格式验证、元数据验证、字节码验证、符号引用验证。
  • 准备(Preparation):

    • 1、为类变量分配内存并设置该类变量的默认初始值,即零值。注意,这里只是分配内存并为其分配默认值,不同类型的默认值也不同。这里并没有对类变量进行实际的具体赋值。
    • 2、这里不包含被final修饰的static变量(这已经不能被称为变量,此时已经是常量),因为final修饰的常量在编译的时候就会分配内存。准备阶段会进行显示初始化,即对其进行具体的赋值。
    • 3、这里不会为实例变量分配初始化(此时对象还未进行创建),类变量会分配在方法区中,而实例变量则是会随着对象一起分配到堆内存中。
  • 解析(Resolution):

    • 1、将常量池内的符号引用转换为直接引用的过程。
    • 2、解析操作往往是会伴随着JVM在执行完初始化后再执行。
    • 3、符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》Class文件格式中。直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。
    • 4、解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。分别对应常量池中的CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等。
public class HelloLinking {

    /**
     * Preparation 阶段只是为其赋初始值0
     * Initialization 阶段将3赋值给a
     */
    private static int a = 3;

    public static void main(String[] args) {
        System.out.println(a);
    }
}

# 3.3、初始化(Initialization)阶段

  • 初始化阶段就是执行类构造器方法()的过程。
  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。如果类中没有变量需要赋值,是不会出现()方法的。
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • ()不同于类的构造器。(关联:构造器是虚拟机视角下的())。
  • 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
  • 虚拟机必须保证一个类的()方法在多线程下被同步加锁。
public class HelloClassLoader {

    public static void main(String[] args) {
        Runnable runnable = () -> {
            System.out.println(getCurrentThreadName() + "开始");
            new DeadThread();
            System.out.println(getCurrentThreadName() + "结束");
        };

        Thread a = new Thread(runnable, "A线程");
        Thread b = new Thread(runnable, "B线程");

        a.start();
        b.start();
    }

    private static class DeadThread {
        static {
            System.out.println(getCurrentThreadName() + "正在初始化 [DeadThread] 类");
        }
    }

    private static String getCurrentThreadName() {
        return Thread.currentThread().getName();
    }

}

最终只会有一个线程获取到锁并初始化DeadThread类。运行程序将会看到只会有一个线程初始化DeadThread类。

B线程开始
A线程开始
B线程正在初始化 [DeadThread] 类
B线程结束
A线程结束

Process finished with exit code 0

所以一个类的()方法只会被一个线程调用一次。


# 4、类加载器分类

JVM支持两种类型的类加载器 。分别为引导类加载器Bootstrap ClassLoader和自定义类加载器User-Defined ClassLoader

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但在Java虚拟机规范中却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个:

  • 引导类加载器(BootStrap Class Loader)
  • 扩展类加载器(Extension Class Loader)
  • 系统类加载器(System Class Loader)

JVM - 类加载器_第4张图片

它们之间的关系是包含关系,并不是上下层或者子父类继承。

可以看到Java中的扩展类加载器ExtClassLoader,它是Launcher中的一个静态内部类。同时也可以获取到AppClassLoader,其也是定义在Launcher类中的一个静态内部类。而BootStrap Class Loader是获取不到的,它是由CC++进行编写的。

使用代码获取加载器:

public class CustomClassLoader {

    public static void main(String[] args) {
        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("systemClassLoader = " + systemClassLoader);

        // 获取其上层即扩展类加载器
        ClassLoader extensionClassLoader = systemClassLoader.getParent();
        // sun.misc.Launcher$ExtClassLoader@1b6d3586
        System.out.println("extensionClassLoader = " + extensionClassLoader);

        // 试图获取 BootStrap Class Loader
        ClassLoader bootStrapClassLoader = extensionClassLoader.getParent();
        // null
        System.out.println("bootStrapClassLoader = " + bootStrapClassLoader);

        // 获取用户自定义类使用的类加载器
        ClassLoader classLoader = CustomClassLoader.class.getClassLoader();
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("classLoader = " + classLoader);

        ClassLoader stringClassLoader = String.class.getClassLoader();
        // null
        System.out.println("stringClassLoader = " + stringClassLoader);

    }

}

输出结果:

systemClassLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
extensionClassLoader = sun.misc.Launcher$ExtClassLoader@1b6d3586
bootStrapClassLoader = null
classLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
stringClassLoader = null

Process finished with exit code 0

对于开发者自定义的类,默认使用的是系统类加载器进行加载。

对于Java的核心类库都是使用的是引导类加载器进行加载。


# 5、虚拟机自带的类加载器

# 1、启动类加载器(引导类加载器,Bootstrap Class Loader)

  • BootStrap类加载使用C/C++语言实现的,嵌套在JVM内部。
  • BootStrap类加载用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jarresources.jarsun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
  • BootStrap类加载并不继承自java.lang.ClassLoader,没有父加载器。
  • BootStrap类加载器加载扩展类和应用程序类加载器,并指定为他们的父类加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为javajavaxsun等开头的类。

2、扩展类加载器(Extension Class Loader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类。
  • 父类加载器为启动类加载器。
  • java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

# 3、应用类加载器(系统类加载器,App Class Loader)

  • Java语言编写,由sun.misc.Launchers$AppClassLoader实现。
  • 派生于ClassLoader类。
  • 父类加载器为扩展类加载器。
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
  • 通过ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器。
public class TestClassLoader {

    private static final String SEMICOLON = ";";
    private static final String EXT_URL = "java.ext.dirs";

    public static void main(String[] args) {
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        Arrays.stream(urLs).forEach(System.out::println);
        System.out.println("\n");

        // 从上面的路劲中随便挑选一个类, 查看其类加载器 :
        ClassLoader proxyClassClassLoader = Proxy.class.getClassLoader();
        // null --> BootStrap Class Loader
        System.out.println("proxyClassClassLoader = " + proxyClassClassLoader);

        System.out.println("\n");
        System.out.println("===========extension class loader spilt line===========");
        
        // 寻找 jre/lib/ext 目录下的 class 获取 ClassLoader
        ClassLoader ecKeyFactoryClassLoader = ECKeyFactory.class.getClassLoader();
        // sun.misc.Launcher$ExtClassLoader@7cca494b
        System.out.println("ecKeyFactoryClassLoader = " + ecKeyFactoryClassLoader);

        System.out.println("\n");
        String extensionUrl = System.getProperty(EXT_URL);
        Arrays.stream(extensionUrl.split(SEMICOLON)).forEach(System.out::println);

        // 获取自定义类的 ClassLoader
        ClassLoader classLoader = CustomClassLoader.class.getClassLoader();
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("classLoader = " + classLoader);
    }

}

# 6、用户自定义类加载器

Java的日常应用程序开发中,类的加载几乎是由BootStrap Class LoaderExtension Class LoaderApp Class Loader类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?

  • 隔离加载类.。

  • 修改类加载的方式。

  • 扩展加载源。

  • 防止源码泄漏。

用户自定义类加载器实现步骤:

  • 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。
  • JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass() 方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass() 方法,而是建议把自定义的类加载逻辑写在findClass()方法中。
  • 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加优雅简洁。

自定义一个CustomClassLoader类加载器派生于ClassLoader

public class CustomClassLoader extends ClassLoader {

    private String classPath;

    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classFromCustomPath = getClassFromCustomPath();
            if (classFromCustomPath == null) {
                throw new FileNotFoundException();
            } else {
                return super.defineClass(name, classFromCustomPath, 0, classFromCustomPath.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        throw new ClassNotFoundException();
    }

    /**
     * 以二进制流的方式将指定的 class 文件读取到系统中来
     * 如果指定路劲的字节码进行了加密, 则需要在此方法中进行解密操作, 解密之后将其还远为字节数组
     *
     * @return byte[]
     */
    private byte[] getClassFromCustomPath() {
        if (this.classPath == null || "".equals(this.classPath)) {
            return null;
        }

        File file = new File(this.classPath);
        if (file.exists()) {
            try (FileInputStream fileInputStream = new FileInputStream(file);
                 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {

                byte[] buffer = new byte[1024];
                int size;
                while ((size = fileInputStream.read(buffer)) != -1) {
                    byteArrayOutputStream.write(buffer, 0, size);
                }
                return byteArrayOutputStream.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

}

创建一个Log类使用自定义类加载器进行加载:

public class Log {

    public static void main(String[] args) {
        System.out.println("Log class load by Custom Class Loader Success!");
    }

}

运行Log类中的main方法,运行完成之后在out文件目录下找到该类的class文件即Log.class文件。

创建一个TestCustomClassLoader类用于测试自定义类加载器:

public class TestCustomClassLoader {

    // Log 类的全类名
    private static final String ALL_PACKAGE_NAME = "com.kapcb.ccc.jvm.classload.Log";
    // Log 类的 class 文件路径
    private static final String LOG_CLASS_PATH = "D:/DevelopTools/IDEA/IDEA-workspace/Java-Kapcb/out/production/Java-Kapcb/com/kapcb/ccc/jvm/classload/Log.class";


    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        CustomClassLoader customClassLoader = new CustomClassLoader(LOG_CLASS_PATH);

        Class<?> LogClass = customClassLoader.loadClass(ALL_PACKAGE_NAME);

        ClassLoader classLoader = LogClass.getClassLoader();
        System.out.println("Log 类的类加载器是 : [ " + classLoader + " ]");

        // 获取 Log 类中的 main 方法
        Method mainMethod = LogClass.getDeclaredMethod("main", String[].class);
        // 实例化 Log 类
        Object object = LogClass.newInstance();
        // 随便传入一个参数
        String[] arg = new String[]{"ad"};
        // 反射激活 Log 类中的 main 方法
        mainMethod.invoke(object, (Object) arg);
    }

}

启动测试类的main方法输出结果:

Log 类的类加载器是 : [ sun.misc.Launcher$AppClassLoader@18b4aac2 ]
Log class load by Custom Class Loader Success!

Process finished with exit code 0

# 7、ClassLoader的常用API

ClassLoader类是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),其常用API有以下几个方法:

方法名称 描述
getParent() 返回当前类加载器的超类加载器
loadClass(String name) 加载名称为name的类(这里的name是全类名),返回结果为java.lang.Class类的实例
findClass(String name) 查找名称为name的类(这里的name是全类名),返回结果为java.lang.Class类的实例
findLoadedClass(String name) 查找类名为name的已经被加载过的类,返回结果为java.lang.Class类的实例
defineClass(String name, byte[] b, int off, int len) 将字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例
resolveClass(Class cla) 链接指定的一个Java

sun.misc.Launcher是一个java虚拟机的入口应用:

JVM - 类加载器_第5张图片

获取ClassLoader的途径:

方式一:获取当前类的ClassLoader

clazz.getClassLoader();

方式二:获取当前线程上下文的ClassLoader

Thread.currentThread().getContextClassLoader();

方式三:获取系统的ClassLoader

ClassLoader.getSystemClassLoader();

方式四:获取调用者的ClassLoader

DriverManager.getCallerClassLoader();

GitHub源码地址:https://github.com/kapbc/Java-Kapcb/tree/master/src/main/java/com/kapcb/ccc/jvm

备注:此文为笔者学习JVM的笔记,鉴于本人技术有限,文中难免出现一些错误,感谢大家批评指正。

你可能感兴趣的:(JVM,jvm)