Java类的加载机制


一、类的生命周期

请添加图片描述
类的生命周期分为5个阶段:加载链接验证准备解析)、初始化使用卸载

其中加载、验证、准备、初始化和卸载这几个的顺序是确定的,类的加载过程必定按上面的流程顺序进行。但解析过程顺序不一定,它在某些情况下可能在初始化阶段后再开始,因为Java支持运行时绑定的。

阅读下文需要注意区分类加载和加载阶段的区别

  1. 类加载:指整个类加载过程,包括:加载阶段、链接阶段、初始化阶段。
  2. 加载阶段:指类的加载过程中的一个阶段(加载阶段)。

二、类的加载过程

一般所说的类的加载过程指的是类的生命周期中的这几个阶段:加载链接(验证、准备、解析)、初始化的过程。

加载时机

加载阶段,Java虚拟机规范中没有进行约束

初始化阶段,Java虚拟机规范中有严格的约束

**初始化前,必须经过加载、验证、准备阶段。**

下面5种情况必须立即进行初始化:

  1. 创建类的实例,也就是new一个对象;获取或者赋值类的静态变量、静态非字面值常量(静态字面值常量除外)。
  2. 调用类的静态方法。
  3. 通过反射调用(Class.forName(“xxx”))。
  4. 初始化一个类的子类(会首先初始化子类的父类)。
  5. 启动程序所使用的main方法所在类。

上面的5中情况称为主动引用,除此主动引用之外还有被动引用。

被动引用有如下3种常见情况:

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  2. 定义对象数组和集合,不会触发该类的初始化。
  3. 类A引用类B的静态常量不会导致类B初始化(注意静态常量必须是字面值常量,否则还是会触发B的初始化)。

其他不会触发初始化的情况:

  1. 通过类名获取Class对象,不会触发类的初始化。如System.out.println(User.class)。
  2. 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化。
  3. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

三、类的加载方式

类加载的方式分为:隐式加载、显式加载。

1. 隐式加载

  • 创建类对象
  • 使用类的静态域
  • 创建子类对象
  • 使用子类的静态域
  • 在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class
  • 在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
  • 在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数所在的类的class文件

2. 显式加载

  • ClassLoader.loadClass(className)

    只加载和连接、不会进行初始化。

  • Class.forName(String name, boolean initialize,ClassLoader loader)

    使用loader进行加载和连接,根据参数initialize决定是否初始化。

四、类加载器

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序如下图:
在这里插入图片描述
加载器

  • BootstrapClassLoader(启动类加载器)

    负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class或者 -Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。

  • ExtensionClassLoader(标准扩展类加载器)

    负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或者 -Djava.ext.dirs指定目录下的jar包。

  • AppClassLoader(系统类加载器)

    负责加载classpath中指定的jar包或者 -Djava.class.path所指定目录下的类和jar包。

  • CustomClassLoader(自定义加载器)

    通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader。如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

加载顺序

  • 加载过程中会先检查类是否被已加载,检查顺序是自底向上,从CustomClassLoader到BootStrapClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
  • 在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
  • Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。

五、阶段详细分析

1. 加载阶段

加载主要是将.class文件通过二进制字节流读入到JVM中。加载是类加载过程的一个阶段,这两个概念一定不要混淆。

加载阶段,虚拟机需要完成以下三件事情:

  1. 通过classloader在classpath中获取XXX.class文件,将其以二进制流的形式读入内存。
  2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个该类的java.lang.Class对象,这样便可以通过该对象访问方法区中的这些数据。

class文件的加载方式:

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

相对于类生命周期的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

2. 链接阶段

当类被加载后,系统为之生成一个对应的Class对象,接着会进入链接阶段,链接阶段将会负责把类的二进制文件合并到JRE中。类的链接阶段分为如下三个阶段:

  • 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致;
  • 准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值;
  • 解析:将类的二进制数据中的符号引用替换成直接引用(符号引用是用一组符号描述所引用的目标;直接引用是指向目标的指针)

1) 验证

确保被加载的类的正确性。

验证阶段主要是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

包括一下几个方面的验证:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,保证方法中的类型转换有效等等。
  • 符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等。

验证阶段是非常重要的,但不是必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施。

2) 准备

为类的静态变量分配内存,并将其赋默认值。

为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。

对于该阶段有以下几点需要注意:

  • 对static修饰的静态变量进行内存分配、并赋默认值(如0、0L、null、false等)。
  • 对final static修饰的静态字面值常量进行内存分配、直接赋初值。
  • 对final static修饰的静态非字面值常量进行内存分配、赋默认值。

3) 解析

将常量池中的符号引用替换为直接引用(内存地址)的过程。

符号引用:一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。

假设:一个类有一个静态变量,该静态变量是一个自定义的类型,那么经过解析后,该静态变量将是一个指针,指向该类在方法区的内存地址。

解析动作主要针对类或接口字段类方法接口方法方法类型方法句柄调用点限定符这7类符号引用进行。

这7类符号引用进行分别对应于常量池的7种常量类型:CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_IntrfaceMethodref_infoCONSTANT_MethodType_infoCONSTANT_MethodHandle_infoCONSTANT_InvokeDynamic_info

3. 初始化阶段

为类的静态变量赋初值。

特别注意:类在初始化前,必须先经过加载、验证、准备阶段

静态变量赋初值的两种方式:

  1. 定义静态变量时指定初始值。

    private static String x="123";
    
  2. 静态代码块里为静态变量赋值。

    private static String x;
    
    static{ 
    	x="123"; 
    } 
    

在编译生成class文件时,编译器会产生两个方法加于class文件中:

  • clinit:类的初始化方法
  • init:实例的初始化方法

类初始化的注意事项:

  1. 类只初始化一次;
  2. 初始化的执行顺序(静态变量、静态代码块)只跟代码中出现的顺序有关(按照声明的顺序执行)。

1. clinit(类的初始化方法)

clinit是类构的造器,主要作用是在类加载过程中的初始化阶段进行执行,执行内容包括:静态变量初始化(赋初值)和静态代码块的执行

注意事项

  • 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成。
  • 在执行clinit方法时,必须先执行父类的clinit方法。
  • clinit方法只执行一次。
  • 静态变量的赋初值和静态代码块和合并顺序由源文件中出现的顺序决定。

2. init(实例的初始化方法)

init是实例的构造器,主要作用是在类实例化过程中执行,执行内容包括:成员变量初始化构造代码块的执行

注意事项

  • 如果类中没有成员变量和构造代码块,那么clinit方法将不会被生成。
  • 在执行init方法时,必须先执行父类的init方法。
  • init方法每实例化一次就会执行一次。
  • init方法先为实例变量分配内存空间,再执行赋默认值,然后进行赋初值和构造代码块的执行(实例变量的赋初值和构造代码块和合并顺序由源文件中出现的顺序决定)。

类初始化的触发条件

  1. 创建类的实例,也就是new一个对象;
  2. 获取或者赋值类的静态变量、静态非字面值常量(静态字面值常量除外);
  3. 调用类的静态方法;
  4. 反射调用(Class.forName(“xxx”))
  5. 初始化一个类的子类(会首先初始化子类的父类)
  6. 启动程序所使用的main方法所在类。

类初始化的步骤

  1. 如果这个类还没有被加载和链接,那先进行加载和链接;

  2. 如果这个类存在直接父类,并且这个直接父类还没有被初始化,先初始化直接父类(不适用于接口) 。

    注意:在一个类加载器中,类只能初始化一次

  3. 如果类中存在初始化语句(如static变量和static代码块),依次执行这些初始化语句。

4. 使用阶段

5. 卸载阶段

如下几种情况下,Java虚拟机将结束生命周期:

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



参考文章:https://blog.csdn.net/zhaocuit/article/details/93038538

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值