1. 类加载过程

1.概述

从类的生命周期而言,一个类包括如下阶段:
在这里插入图片描述
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序进行,而解析阶段则不一定,它在某些情况下可能在初始化阶段后在开始,因为java支持运行时绑定。

2. 类加载时机
加载(loading)阶段,java虚拟机规范中没有进行约束,但初始化阶段,java虚拟机严格规定了  
有且只有如下5种情况必须立即进行初始化(初始化前,必须经过加载、验证、准备阶段):  
(1)使用new实例化对象时,读取和设置类的静态变量、静态非字面值常量(静态字面值常量除外)时,调用静态方法时。
(2)对内进行反射调用时。
(3)当初始化一个类时,如果父类没有进行初始化,需要先初始化父类。
(4)启动程序所使用的main方法所在类
(5)当使用1.7的动态语音支持时。

如上5种场景又被称为主动引用,除此之外的引用称为被动引用,被动引用有如下3种常见情况:

通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
定义对象数组和集合,不会触发该类的初始化
类A引用类B的static final常量不会导致类B初始化(注意静态常量必须是字面值常量,否则还是会触发B的初始化)
public class TestClass {
    public static void main(String[] args) {
        System.out.println(ClassInit.str);
        System.out.println(ClassInit.id);
    }
}
class ClassInit{
    public static final long id=IdGenerator.getIdWorker().nextId();//需要初始化ClassInit类
    public static final String str="abc";//字面值常量
    static{
        System.out.println("ClassInit init");
    }
}
通过类名获取Class对象,不会触发类的初始化。如System.out.println(Person.class);
通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化。
通过ClassLoader默认的loadClass方法,也不会触发初始化动作
注意:被动引用不会导致类初始化,但不代表类不会经历加载、验证、准备阶段。  
3. 类加载方式

这里的类加载不是指类加载阶段,而是指整个类加载过程,即类加载阶段到初始化完成。

(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决定是否初始化。

2. 加载阶段

加载是类加载过程中的一个阶段,不要将这2个概念混淆了。
在加载阶段,虚拟机需要完成以下3件事情:

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

加载.class文件的方式:

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

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

3. 连接阶段

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

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

文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,  
          是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,  
          保证方法中的类型转换有效等等。
符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等。
验证阶段是非常重要的,但不是必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施。  
3.2 准备:为类的静态变量分配内存,并将其赋默认值

为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。
对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)。
3.3 解析:将常量池中的符号引用替换为直接引用(内存地址)的过程

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

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

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

4. 初始化:为类的静态变量赋初值

赋初值两种方式:
     1.定义静态变量时指定初始值。如 private static String x="123";
     2.在静态代码块里为静态变量赋值。如 static{ x="123"; }      

5. clinit 与 init

在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init。

5.1 clinit

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

  1. 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成。
  2. 在执行clinit方法时,必须先执行父类的clinit方法。
    3.clinit方法只执行一次。
  3. static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定。如下代码所示:
public class TestClass {
    public static void main(String[] args) {
        ClassInit init=ClassInit.newInstance();
 
        System.out.println(init.x);
        System.out.println(init.y);
    }
}
 
class ClassInit{
    private static ClassInit init=new ClassInit();
    public static int x;
    public static int y=0;
    static{
        x=10;
        y=10;
    }
    private ClassInit(){
        x++;
        y++;
    }
    public static ClassInit newInstance(){
        return init;
    }
}
//在类加载到连接完成阶段,ClassInit类在内存中的状态为:init=null,x=0,y=0
//初始化阶段时,需要执行clinit方法,该方法类似如下伪代码:
clinit(){
	//init=new ClassInit();调用构造方法
    x++;//x=1 因为此时x的值为连接的准备阶段赋的默认值0,然后++变成1
    y++;//y=1 因为此时y的值为连接的准备阶段赋的默认值0,然后++变成1
    //x=0;//为什么这里没有执行x=0,因为程序没有给x赋初值,因此在初始化阶段时,不会执行赋初值操作
    y=0;//因为类变量y在定义时,指定了初值,尽管初值为0,因此在初始化阶段的时候,需要执行赋初值操作
    x++;//第一个静态块的自增操作,结果为x=2;
    y++;//第一个静态块的自增操作,结果为y=1;
}
//所以最终结果为x=2,y=1
//如果private static ClassInit init=new ClassInit(); 代码在public static int y=0;后面,那么clinit方法的伪代码如下:
clinit(){
    //x=0;//这里虽然没有执行,但此时x的值为连接的准备阶段赋的默认值0
    y=0;//因为类变量y在定义时,指定了初值,尽管初值为0,因此在初始化阶段的时候,需要执行赋初值操作
	//init=new ClassInit();调用构造方法
    x++;//x=1 因为此时x的值为连接的准备阶段赋的默认值0,然后++变成1
    y++;//y=1 因为此时y的值为初始化阶段赋的初值,只是这个初值刚好等于默认值0而已,然后++变成1
    x++;//第一个静态块的自增操作,结果为x=2;
    y++;//第一个静态块的自增操作,结果为y=2;
}
//最终结果为x=2,y=2
5.2 init

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

  1. 如果类中没有成员变量和代码块,那么clinit方法将不会被生成。

  2. 在执行init方法时,必须先执行父类的init方法。

  3. init方法每实例化一次就会执行一次。

  4. init方法先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块。如下代码所示:

public class TestClass {
   public static void main(String[] args) {
       ClassInit init=new ClassInit();
   }
}

class ClassInit{
   public int x;
   public int y=111;
   public ClassInit(){
       x=1;
       y=1;
   }
   {
       x=2;
       y=2;
   }
   {
       x=3;
       y=3;
   }
}
//实例化步骤为:先为属性分配空间,再执行赋默认值,然后按照顺序执行代码块或赋初始值,最后执行构造方法
//根据上述代码,init方法的伪代码如下:
init(){
   x=0;//赋默认值
   y=0;//赋默认值
   y=111;//赋初值
   x=2;//从上到下执行第一个代码块
   y=2;//从上到下执行第一个代码块
   x=3;//从上到下执行第二个代码块
   y=3;//从上到下执行第二个代码块
   //ClassInit();执行构造方法
   x=1;//最后执行构造方法
   y=1;//最后执行构造方法
}
//如果上述代码的成员变量x,y的定义在类最后时,那么init方法的伪代码如下:
init(){
   x=0;//赋默认值
   y=0;//赋默认值
   x=2;//从上到下执行第一个代码块
   y=2;//从上到下执行第一个代码块
   x=3;//从上到下执行第二个代码块
   y=3;//从上到下执行第二个代码块
   y=111;//赋初值
   //ClassInit();执行构造方法
   x=1;//最后执行构造方法
   y=1;//最后执行构造方法
}

6. 卸载阶段

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

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐