Java 类加载器详解

Java 类加载器

  • 什么是类的加载
  • 类的加载过程
    • 类的加载过程(生命周期)
    • 加载.class文件的方式
    • 加载
    • 验证:确保被加载的类的正确性。
    • 准备:为类的静态变量分配内存,并将其初始化为默认值/零值或常量值。
    • 解析:把类中的符号引用转换为直接引用
    • 初始化(类)
    • 使用
    • 卸载:结束生命周期
  • 类加载器
    • JVM类加载机制
  • 类的初始化
    • 类加载方式
    • JVM初始化步骤
    • 对象初始化方式
    • 参考资料
  • 对象的初始化
    • 对象初始化过程
  • 双亲委派模型
  • 自定义类加载器

什么是类的加载

类的加载指的是将类的.class文件中的二进制数据读入到方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

Java 类加载器详解_第1张图片

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError、ClassNotFoundException错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

类的加载过程

类的加载过程(生命周期)

Java 类加载器详解_第2张图片

其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

加载.class文件的方式

从本地系统中直接加载
通过网络下载.class文件
从zip,jar等归档文件中加载.class文件
从专有数据库中提取.class文件
将Java源文件动态编译为.class文件

加载

查找并加载类的二进制数据加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
    相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

验证:确保被加载的类的正确性。

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作,验证包含以下内容:
• 文件格式验证:
验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
• 元数据验证:
对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
• 字节码验证:
通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
• 符号引用验证:
符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

准备:为类的静态变量分配内存,并将其初始化为默认值/零值或常量值。

https://jacobchang.cn/preparation-and-init-of-classloader.html
准备阶段主要工作:正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
这里还需要注意如下几点:
• 类变量(static )和 全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的值。
• 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过。
• 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
• 局部变量不需初始化。
常量(static final 直接赋值)。
如:假设上面的类变量value被定义为: public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中

解析:把类中的符号引用转换为直接引用

解析阶段:虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化(类)

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
① 声明类变量是指定初始值。
② 使用静态代码块为类变量指定初始值。
JVM类初始化步骤:
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

• 创建类的实例,也就是new的方式
• 访问某个类或接口的静态变量,或者对该静态变量赋值
• 调用类的静态方法
• 反射(如Class.forName(“com.shengsiyuan.Test”))
• 初始化某个类的子类,则其父类也会被初始化
• Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

使用

使用类进行业务操作。

卸载:结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期
执行了System.exit()方法。
程序正常执行结束。
由于操作系统出现错误而导致Java虚拟机进程终止。

类加载器

类加载器的层次关系如下图所示:
Java 类加载器详解_第3张图片

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:
• 启动类加载器:
它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;
• 所有其它的类加载器:
这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
站在Java开发人员的角度来看,类加载器可以大致划分为以下四类:
• 启动类加载器:
Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
• 扩展类加载器:
Extension ClassLoader,该加载器由sun.misc.LauncherKaTeX parse error: Undefined control sequence: \jre at position 26: …ader实现,它负责加载JDK\̲j̲r̲e̲\lib\ext目录中,或者由…AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
• 用户自定义类加载器:
用户根据自己业务需求自己定义的加载器,比如可通过网络、zip等加载类。
应用程序都是由这几种类加载器互相配合进行加载的,因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

1、在执行非置信代码之前,自动验证数字签名。
2、动态地创建符合用户特定需要的定制化构建类。
3、从特定的场所取得java class,例如数据库中和网络中。

JVM类加载机制

全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

类的初始化

类加载方式

1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载(默认会执行初始化)
3、通过ClassLoader.loadClass()方法动态加载 (不会执行初始化。)
Class.forName()和ClassLoader.loadClass()区别
Class.forName():将类的.class文件加载到jvm中之外,还会对类进行初始化;
ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行类的初始化,只有在newInstance才会去执行类的初始化。
Class.forName(name, initialize, loader)带参函数也可控制是否执行初始化。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

JVM初始化步骤

1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句类初始化时机Java程序对类的使用方式可以分为两种:
1.主动使用
2.被动使用
只有当对类的主动使用的时候才会导致类的初始化。
类的主动使用包括以下六种:
Java 类加载器详解_第4张图片

类的被动使用
参考https://blog.csdn.net/mweibiao/article/details/79516708

引起类加载操作的五个行为
遇到new、getstatic、putstatic或invokestatic这四条字节码指令
反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
子类初始化的时候,如果其父类还没初始化,则需先触发其父类的初始化
虚拟机执行主类的时候(有 main(string[] args))

对象初始化方式

1). 使用new关键字创建对象
2). 使用Class类的newInstance方法(反射机制)
3). 使用Constructor类的newInstance方法(反射机制)
4). 使用Clone方法创建对象
5). 使用(反)序列化机制创建对象

参考资料

https://www.jianshu.com/p/e33715c979af

对象的初始化

对象初始化过程

Java 类加载器详解_第5张图片

参考资料
https://cloud.tencent.com/developer/article/1177048

双亲委派模型

双亲委派模型的工作流程:
简单的讲:类加载优先父类加载器加载,若无法完成加载则有自身的类加载器再进行加载。
双亲委派机制:
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

双亲委派模型意义:
系统类防止内存中出现多份同样的字节码,保证不同类加载器加载的类均相同。
通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常。

Java类加载机制
双亲委托机制的作用:
共享功能:可以避免重复加载,当父亲已经加载了该类的时候,子类不需要再次加载,一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。
隔离功能:因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的类装载器,保证java/Android核心类库的纯净和安全,防止恶意加载。
如何打破双亲委派模型?
双亲委派模型的逻辑都在loadClass()中,重写loaderClass(),一般是重写findClass()的
系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载

参考资料
https://blog.csdn.net/u011080472/article/details/51332866

自定义类加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写 findClass 方法即可。
自定义类加载器的写法参考资料
https://www.cnblogs.com/xrq730/p/4847337.html

自定义ClassLoader:
loadClass(String name,boolean resolve):根据指定的二进制名称加载类
findClass(String name): 根据二进制名称来查找类
直接使用或继承已有的ClassLoader实现:java.net.URLClassLoader、java.security.SecureClassLoader、 java.rmi.server.RMIClassLoader

在调用loadClass(),会先根据委派模型在父加载器中加载,如果加载失败,则会调用自己的findClass方法来完成加载

  private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("E:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.neo.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

1、这里传递的文件名需要是类的全限定性名称,即com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。
2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
3、这类Test 类本身可以被 AppClassLoader类加载,因此我们不能把com/paddx/test/classloading/Test.class放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载。

你可能感兴趣的:(Java后端技术栈,#,进阶语法与原理)