类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象【也就是我们映射时使用的Class对象】,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。[简单来说:Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。]
java类加载机制 - 图1
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

类加载过程

首先类加载的前提是.class文件,并不是.java文件,而java文件到class文件的过程,我们称之为编译过程

JDK/JRE/JVM

JDK 8是JRE 8的超集,包含了JRE 8中的所有内容,编译器和调试器等开发applet和应用程序。JRE 8提供了库、Java虚拟机(JVM)和运行用Java编程编写的applet和应用程序的其他组件语言。注意,JRE包含了Java SE不需要的组件,规范,包括标准和非标准Java组件。
官网

java类加载机制 - 图2
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。

java文件编译过程

编译的过程大致可以分为以下几个步骤:

  1. 编写完成的java文件,Person.java
  2. 词法分析器
  3. tokens流
  4. 语法分析器
  5. 语法树/抽象语法树
  6. 语义分析器
  7. 注解抽象语法树
  8. 字节码生成器
  9. 最后获得.class文件

Class文件是一组以8位字节为基础单位的二进制流。每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Calss文件。也就是 CA FE BA BE 这四个字节。类似于身份标识

类加载过程图

JVM 将类的加载过程分为三个大的步骤:加载(loading),链接(link),初始化(initialize)。其中链接又分为三个步骤:验证,准备,解析。
java类加载机制 - 图3

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

类加载(loading)

  • 查找并加载编译完成的二进制流.class文件
  • 在类加载阶段,虚拟机需要完成三件事情:
    • 通过一个类的全限定名来获取其定义的二进制字节流【完全限定名找到二进制流文件】
    • 将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构【将数据加载到内存中】
    • 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口【java反射】

注意:

  • 加载阶段获取类的二进制字节流的动作,是通过类加载器来完成的,类加载我们既可以使用系统提供的类加载器来完成也可以自定义自己的类加载器来完成加载。【这个阶段对于开发者来说是我们可以做很多事,比如创建钩子任务等】
  • 系统提供的加载器有:ClassLoader

    链接(link)

    链接分为三部分:验证、准备、解析

  1. 验证【确保加载的类的正确性】

    确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

注意:验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

  1. 准备【为类的静态变量分配内存,并将其初始化为默认值】

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配,需要注意:

  • 这个阶段分配的内存仅包括Static修饰的静态变量,在jvm方法区

例如:

假设一个类变量的定义为:public static int value = 3;

那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰的变量,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

  1. 解析【将常量池中的符号引用替换为直接引用的过程】

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化(initialize)

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化

JVM初始化步骤

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化

  • 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

    生成这4条指令的最常见的Java代码场景是:

    1. 使用new关键字实例化对象的时候
    2. 读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候
    3. 调用一个类的静态方法的时候
  • 使用 java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

结束生命周期【Java虚拟机将结束生命周期】

  • 执行System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

    类装载器ClassLoader

    JVM 类加载器作用,将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。类加载器是通过ClassLoader 及其子类【启动类加载器由C编写,其它有java自身编写,独立于虚拟机之外,全部继承ClassLoader】来完成的,类的层次关系和加载顺序可以由下图来描述:
    java类加载机制 - 图4
  1. 启动类加载器【Bootstrap ClassLoader】 :负责加载$JAVA_HOMEjre/lib/rt.jar 里所有的class或 -Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。启动类加载器是无法被Java程序直接引用的
  2. 扩展类加载器【Extension ClassLoader】:由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器【Application ClassLoader】:由sun.misc.Launcher$AppClassLoader实现,负责加载classpath中指定的jar包及 java.class.path 所指定目录下的类和 jar包
  4. 自定义类加载器【User ClassLoader】: 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

    JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

  5. 在执行非置信代码之前,自动验证数字签名。

  6. 动态地创建符合用户特定需要的定制化构建类。
  7. 从特定的场所取得java class,例如数据库中和网络中。

类加载原则

检查某个类是否已经加载:顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个Classloader已加载,就视为已加载此类,保证此类只所有ClassLoader加载一次。加载的顺序:加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类

类加载机制

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

获取当前java使用的加载器

  1. public static void main(String[] args) {
  2. //new 对象
  3. User1 user1 = new User1();
  4. //通过 getClass方法,getClassLoader即可获取类加载器 反射相同
  5. System.out.println("获取当前对象的类加载器 " + user1.getClass().getClassLoader());
  6. System.out.println("获取当前对象的类加载器的父类加载器 " + user1.getClass().getClassLoader().getParent());
  7. System.out.println(user1.getClass().getClassLoader().getParent().getParent());
  8. /**
  9. * 输出结果:
  10. * 获取当前对象的类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
  11. * 获取当前对象的类加载器的父类加载器 sun.misc.Launcher$ExtClassLoader@2f2c9b19
  12. * null // Bootstrap Loader(引导类加载器)是用C++语言实现,java无法获取
  13. *
  14. */
  15. }

双亲委派机制

定义源码分析

定义: 一个类加载器收到类加载的请求,并不会先自己尝试加载该类,而是会向上传递,直至最高级也就是BootStrapClassLoader类加载器去尝试加载该类,当父类加载该类失败后,会向下传递,尝试加载该类。

总结:请求会自下往上传递,加载类动作会自上往下尝试。

java类加载机制 - 图5

优势:

  • Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己去加载的话,那么系统中会存在多种不同的Object类。
  • 系统类防止内存中出现多份同样的字节码,保证Java程序安全稳定运行

jdk1.8源码
ClassLoader 的 loadClass(String name, boolean resolve)

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // First, check if the class has already been loaded
  6. //首先,检查这个class是否已经被加载
  7. Class<?> c = findLoadedClass(name);
  8. if (c == null) {// 等于null,没有被加载过
  9. long t0 = System.nanoTime();
  10. try {
  11. if (parent != null) {//父加载器是否为空
  12. c = parent.loadClass(name, false);//存在父加载器,使用父加载器加载
  13. } else {//若其父类加载器为null,则说明本类加载器为扩展类加载器,父类加载器为启动类加载器,尝试使用bootstrap classloader进行类的加载
  14. c = findBootstrapClassOrNull(name);
  15. }
  16. } catch (ClassNotFoundException e) {
  17. // ClassNotFoundException thrown if class not found
  18. // from the non-null parent class loader
  19. }
  20. if (c == null) {
  21. //若c为空,则父类加载器加载失败
  22. // If still not found, then invoke findClass in order
  23. // to find the class.
  24. long t1 = System.nanoTime();
  25. c = findClass(name);//尝试使用自定义类加载器进行加载
  26. // this is the defining class loader; record the stats
  27. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  28. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  29. sun.misc.PerfCounter.getFindClasses().increment();
  30. }
  31. }
  32. if (resolve) {//通过传入的标识来控制是否要对该类进行初始化操作
  33. resolveClass(c);
  34. }
  35. return c;
  36. }
  37. }

自定义类加载器

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

  1. package com.zy.gm.classLoader;
  2. import java.io.*;
  3. /**
  4. * @author Administrator
  5. * @date 2021/11/29 14:23
  6. **/
  7. public class MyClassLoader extends ClassLoader{
  8. private String root;
  9. protected Class<?> findClass(String name) throws ClassNotFoundException {
  10. byte[] classData = loadClassData(name);
  11. if (classData == null) {
  12. throw new ClassNotFoundException();
  13. } else {
  14. return defineClass(name, classData, 0, classData.length);
  15. }
  16. }
  17. private byte[] loadClassData(String className) {
  18. String fileName = root + File.separatorChar
  19. + className.replace('.', File.separatorChar) + ".class";
  20. try {
  21. InputStream ins = new FileInputStream(fileName);
  22. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  23. int bufferSize = 1024;
  24. byte[] buffer = new byte[bufferSize];
  25. int length = 0;
  26. while ((length = ins.read(buffer)) != -1) {
  27. baos.write(buffer, 0, length);
  28. }
  29. return baos.toByteArray();
  30. } catch (IOException e) {
  31. e.printStackTrace();
  32. }
  33. return null;
  34. }
  35. public String getRoot() {
  36. return root;
  37. }
  38. public void setRoot(String root) {
  39. this.root = root;
  40. }
  41. public static void main(String[] args) {
  42. MyClassLoader classLoader = new MyClassLoader();
  43. //磁盘绝对地址 如果加载的文件没有放在类目录下,则需要添加绝地址前缀
  44. // classLoader.setRoot("D:\\idea-develop-project\\Project_All\\springboot-dome\\src\\main\\java\\");
  45. Class<?> testClass = null;
  46. try {
  47. //需要加载的文件的包地址
  48. testClass = classLoader.loadClass("com.zy.gm.orika.User1");
  49. Object object = testClass.newInstance();
  50. System.out.println("object = " + object);
  51. //使用的加载器
  52. System.out.println(object.getClass().getClassLoader());
  53. } catch (ClassNotFoundException e) {
  54. e.printStackTrace();
  55. } catch (InstantiationException e) {
  56. e.printStackTrace();
  57. } catch (IllegalAccessException e) {
  58. e.printStackTrace();
  59. }
  60. }
  61. }

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

  1. 这里传递的文件名需要是类的全限定性名称,即com.zy.gm.orika.User1格式的,因为 defineClass方法是按这种格式进行处理的。
  2. 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。

实战分析

示例一

  1. class School {
  2. static {
  3. System.out.println("School 静态代码块");
  4. }
  5. }
  6. class Teacher extends School {
  7. static {
  8. System.out.println("Teacher 静态代码块");
  9. }
  10. public static String name = "Tony";
  11. public Teacher() {
  12. System.out.println("I'm Teacher");
  13. }
  14. }
  15. class Student extends Teacher {
  16. static {
  17. System.out.println("Student 静态代码块");
  18. }
  19. public Student() {
  20. System.out.println("I'm Student");
  21. }
  22. }
  23. class InitializationDemo {
  24. public static void main(String[] args) {
  25. System.out.println("Teacher's name: " + Student.name); //入口
  26. }
  27. }

输出结果:

  1. School 静态代码块
  2. Teacher 静态代码块
  3. Teacher's name: Tony

可以看到输出结果并没有“Student 静态代码块”
原因:
对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
也就是说,类加载器并没有初始化学生,我们是通过学生调用的老师的name字段,所有只初始化了学校和老师

类加载过程:

  1. 首先程序到 main 方法这里,使用标准化输出 Student 类中的 name类成员变量,但是 Student类中并没有定义这个类成员变量。于是往父类去找,我们在 Teacher类中找到了对应的类成员变量,于是触发了 Teacher的初始化。
  2. 但是因为Teacher类继承了school类,所有需要先初始化 School 类再初始化 Teacher类。于是我们先初始化 School 类输出:School 静态代码块,再初始化 Teacher类输出:Teacher 静态代码块。
  3. 最后,所有父类都初始化完成之后,Student 类才能调用父类的静态变量,从而输出:Teacher’s name Tony

示例二

  1. public class classloader01 {
  2. public static void main(String[] args) {
  3. new Student();
  4. }
  5. }
  6. class School {
  7. static {
  8. System.out.println("School 静态代码块");
  9. }
  10. public School() {
  11. System.out.println("I'm School");
  12. }
  13. }
  14. class Teacher extends School {
  15. static {
  16. System.out.println("Teacher 静态代码块");
  17. }
  18. public Teacher() {
  19. System.out.println("I'm Teacher");
  20. }
  21. }
  22. class Student extends Teacher {
  23. static {
  24. System.out.println("Student 静态代码块");
  25. }
  26. public Student() {
  27. System.out.println("I'm Student");
  28. }
  29. }
  30. //输出结果:
  31. /**
  32. School 静态代码块
  33. Teacher 静态代码块
  34. Student 静态代码块
  35. I'm School
  36. I'm Teacher
  37. I'm Student
  38. **/

分析:

  • 实例化一个 Student 对象
  • 触发student父对象初始化,输出父对象中的静态代码块:School 静态代码块、Teacher 静态代码块、Student 静态代码块
  • 类初始化完成,调用构造方法,而 Student 类构造方法的调用同样会带动 Teacher、School 类构造方法的调用,输出I’m School、I’m Teacher、I’m Student

    示例三

    ```java

class Teacher { public static void main(String[] args) { staticFunction(); } static Teacher teacher = new Teacher(); static { System.out.println(“teacher 静态代码块”); } { System.out.println(“teacher普通代码块”); } Teacher() { System.out.println(“teacher 构造方法”); System.out.println(“age= “ + age + “,name= “ + name); } public static void staticFunction() { System.out.println(“teacher 静态方法”); } int age = 24; static String name = “Tony”;

  1. /**
  2. * teacher普通代码块
  3. * teacher 构造方法
  4. * age= 24,name= null
  5. * teacher 静态代码块
  6. * teacher 静态方法
  7. *
  8. */

}

  1. **类加载过程分析**
  2. - JVM 在准备阶段的时候,便会为类变量分配内存和进行初始化。此时,我们的 teacher 实例变量被初始化为 nullname 变量被初始化为 null
  3. - 当进入初始化阶段后,因为 Teacher() 方法是程序的入口,先初始化main方法的主类,对teacher进行初始化
  4. - JVM Teacher 类进行初始化首先是执行类构造器(按顺序收集类中所有静态代码块和类变量赋值语句就组成了类构造器),后执行对象的构造器(先收集成员变量赋值,后收集普通代码块,最后收集对象构造器,最终组成对象构造器)。
  5. 首先按代码顺序收集所有静态代码块和类变量进行赋值,即执行以下代码并且将 name 初始化为 null
  6. ```java
  7. static Teacher teacher = new Teacher();
  8. static {
  9. System.out.println("teacher 静态代码块");
  10. }
  11. static String name = "Tony"; // 静态变量准备阶段只会分配内存,并不会初始化,初始化时为null

而这里触发了对象的构造器(先收集成员变量赋值,后收集普通代码块,最后收集对象构造器,最终组成对象构造器),从而执行:

  1. int age = 24; //将age赋值为24
  2. {
  3. System.out.println("teacher普通代码块");
  4. }
  5. Teacher() {
  6. System.out.println("teacher 构造方法");
  7. //此时 name 还没赋值,所以是null
  8. System.out.println("age= " + age + ",name= " + name);
  9. }

最后执行

  1. static {
  2. System.out.println("teacher 静态代码块");
  3. }
  4. //此刻执行name赋值为 Tony 的操作
  5. public static void staticFunction() {
  6. System.out.println("teacher 静态方法");
  7. }

总结

  1. 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
  2. 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器,之后初始化对象构造器
  3. 初始化类构造器。初始化类构造器是初始化类的第一步,其会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
  4. 初始化对象构造器。初始化对象构造器是在类构造器执行完成之后的第二步操作,其会按照执行类成员变成赋值、普通代码块、对象构造方法的顺序收集代码,最终组成对象构造器,最终由 JVM 执行。
  5. 如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么继续按照初始化类构造器、初始化对象构造器的顺序继续初始化。如此反复循环,最终返回 main 方法所在类。

参考:
Java类加载机制
java类的加载机制
深入研究Java类加载机制