[JVM]专题:类加载过程

[JVM]专题:类加载过程

类的加载过程简介

在这里插入图片描述

类的加载过程一般分为三个大阶段,加载阶段、链接阶段、初始化阶段。

在这里插入图片描述

  • 加载阶段:主要负责查找并加载类的二进制数据文件,其实就是class文件
  • 链接阶段:链接阶段所做的工作多,细分为如下三个阶段。
    • 验证:主要是确保类文件的正确性,比如class的版本,class文件的魔数是否正确
    • 准备:为类的静态变量分配内存,并且为其初始化默认值
    • 解析:把类的符号引用转换为直接引用
  • 初始化阶段:为类的静态变量赋予正确的初始值(代码编写阶段给定的值)

当一个JVM在我们通过执行Java命令启动后,其中可能包含的类非常多,但并非每一个类都会被初始化。JVM对类的初始化是一个延迟的机制,即使用的是lazy的方式,当一个雷在首次使用的时候才会被初始化,在同一个运行时包下,一个Class只会被初始化一次(运行时包和类的包有差别)

类的主动使用和被动使用

JVM虚拟机规范规定:每个类或者接口被Java程序首次主动使用时才会对其进行初始化。有6种主动使用类的场景。

  • 通过关键字new 导致类的初始化。

  • 访问类的静态变量,包括读取和更新会导致类的初始化,这种情况如下:

    public class Simple{
        static{
            System.out.println("I will be initialized");
        }
        public static int x = 10;
    }
    

    这段代码x是一个简单的静态变量,其他类即使不对Simple进行new 创建,直接访问变量x也会导致类的初始化。

  • 访问类的静态方法,会导致类的初始化,这种情况如下:

    public class Simple{
        static{
            System.out.println("I will be initialized");
        }
        public static void test(){
            
        }
    }
    

    在其他类直接调用test方法也会导致类的初始化。

  • 对某个类进行反射操作,会导致类的初始化,这种情况如下:

    public static void main (String[] args) throws ClassNotFoundException{
        Class.forName("...");
    }
    
  • 初始化紫烈会导致父类的初始化

    需要注意的是:通过子类使用父类的静态变量只会导致父类的初始化,子类则不会被初始化

  • 启动类:也就是执行main函数所在的类会导致该类的初始化。

除了上述6种情况外,其他的都属于被动使用,不会导致类的加载和初始化。下面是两个容易混淆的例子:

  • 构造某个类的数组时不会导致该类的初始
public static void main (String[] args){
    Simple[] simples = new Simple[10];
    System.out.println(simple.length);
}

上述代码new方法新建了一个Simple类型的数组,但是它并不能导致Simple的初始化,因此它是被动使用。

不要被前面的new 关键字误导。事实上该操作只不过是在内存中开辟了一段连续的地址空间

  • 引用类的静态变量不会导致类的初始化

    public class GlobalConstants{
        static{
            System.out.println("The GlobalConstant will be initialized!");
        }
        //在其他类中使用MAX不会到孩子GlobalConstants的初始化,静态代码块不会输出
        public final static int MAX = 100;
        
        //虽然RANDOM是静态常量,但是由于计算复杂,只有初始化之后才能得到结果,因此在其他类中使用RANDOM会导致GlobalConstants的初始化
        public final static int RANDOM = new Random().nextInt();
    }
    

    这段代码MAX是一个被final修饰的静态变量,也就是一个静态常量,在其他类中直接方法MAX不会导致GlobalConstants的初始化。

    但是如果在其他类中访问RANDOM会导致类的初始化,因为RANDOM是需要进行随机函数计算的,在类的加载、链接是无法进行计算的,需要进行初始化后才能对其赋予准确的值。

类加载过程详解

类的加载loading阶段

简单来说,类的加载就是将class文件中的二进制数据读取到内存之中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.Class对象,作为访问方法区数据结构的入口。抽理如下:

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

在这里插入图片描述

类加载的最终产物就是堆内存中的class对象,对同一个ClassLoader来讲,不管某个类被加载了多少次,对应到堆内存中的calss对象始终是同一个。

加载.class文件的方式:

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

类的连接linking阶段

验证(Verify)
  • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。当字节流的信息不符合要求时,则会抛出VerifyError这样的异常。
  • 主要包括四种验证:
    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证
准备(prepare)

当一个class的字节流通过了所有的验证阶段之后,就开始为该对象的类变量,也就是静态变量,分配内存并设置初始值了,类变量的内存会被分配到方法区中,不同于实例变量被分配到堆内存中。

所谓设置初始值,其实就是为相应的类变量给定一个相关类型在没有被设置值时的默认值。

public class LinkedPrepare{
    private static int a = 10;
    private final static int b = 10;
}

其中static int a = 10在准备阶段不是10,而是初始值0,当然final static int b 则还会是10。这是因为final修饰的静态变量(可直接计算得出结果)不会导致类的初始化,是一种被动引用,因此就不存在连接阶段了。

准确来说,final static int b = 10在类的编译阶段javac会将其value生成一个ConstantValue 属性,直接赋予10

解析(Resolve)

在连接阶段经历了验证、准备之后,就可以顺利进入到解析过程了。当然在 解析过程中照样会交叉一些验证过程,比如符号引用的验证,所谓解析就是在常量池中寻找类、

接口、字段和方法的符号引用,并且将这些符号引用替换成直接引用的过程:

public class ClassResolve{
    static Simple simple = new Simple();
    public satatic void main(String[] args){
        System.out.println(simple);
    }
}

在上面的代码中ClassResolve用到了Simple类,在编写程序时,可以直接使用simple这个引用去访问Simple类中可见的属性和方法,但是在class字节码中,它会被编译成相应的助记符,这些助记符称为符号引用,这类的解析过程中,助记符还需要得到进一步解析,才能正确地找到所对应的堆内存中的Simple数据结构。

将常量池的符号引用转换为直接引用的过程

  • 事实上,解析操作往往会伴随着JVM在执行完初始化之后在执行
  • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。
    直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info等

初始化

在初始化阶段最主要的一件事情即时执行<Clinit>()方法的过程,在<Clinit>()中所以的类的变量都会被赋予正确的值,也就是在程序编写的时候指定的值。

<Clinit>()方法是在编译阶段生成的,也就是说它已经包含在了class文件中了,<Clinit>()中包含了所有类变量的赋值动作和静态语句块执行代码,编译期收集的顺序是由执行语句在源文件中的出现顺序所决定。

  • 初始化阶段就是执行类构造器方法< Clinit >()的过程
  • 此方法不需要定义,是javac 编译器自动收集类中的所有类变量的赋值动作静态代码块中的语句合并而来
  • 构造器方法中指令按语句在源文件中出现的顺序执行
  • <clinit>() 不同于类的构造器。(构造器是虚拟机视角下的<init>()
  • 若该类具有父类,JVM会保证子类的<Clinit>()执行前,父类的<Clinit>()已经执行完毕
  • 虚拟机必须保证一个类的<Clinit>()方法在多线程下被同步加锁
public class ClassInitTest {
    private static  int num = 1;
    
    static {
        num = 2;
        number = 20;
        System.out.println(num);
//        System.out.println(number); // error: 非法的前向引用
    }
    
    private static int number = 10; //Linking中Prepare: number = 0 --> initial: 20 --> 10

    public static void main(String[] args) {
        System.out.println(ClassInitTest.num);  //2
        System.out.println(ClassInitTest.number);//10

    }

注意:尽管看上去number声明定义并立刻赋值为10,但是实际上:

  1. number是先在链接中的准备阶段被分配内存并设置为初始值 0。
  2. 在初始化阶段按照static块顺序依次赋值为20 然后是 10。

注意:第8行是怎么回事呢?

声明的变量number在后面,在前面的静态代码块中可以为其赋值,但是不能调用它

例子

public class Singleton{
    //1
    private static int x = 0;
    private static int y;
    private static Singleton instance = new Singleton(); //2
    
    private Singleton(){
        x ++ ;
        y ++ ;
    }
    
    public static void main(String[] args){
        Singleton singletion = Singleton.getInstance();
        System.out.println(singleton.x);
        System.out.println(singleton.y);
    }
}

上述代码打印的信息如何?当2处代码放到1处时,打印结果又是如何?

  1. 当前情况

    private static int x = 0;
    private static int y;
    private static Singleton instance = new Singleton(); //2
    

    在连接过程中,每一个类变量都被赋予了相应的初始值:

    x = 0 , y = 0 , instance = null
    

    下面跳到解析过程,来看类的初始化阶段,初始化阶段会为每一个类变量赋予正确的值,也就是执行<clint>()方法的过程“

    x = 0 , y = 0 , instance = new Singleton()
    

    在new Singleton的时候会执行类的构造函数,而在构造函数中分别对x和y进行了自增,因此结果:

    x = 1, y = 1
    
  2. 调换顺序

    private static Singleton instance = new Singleton(); //2
    private static int x = 0;
    private static int y;
    

    在连接过程中,每个类变量都被赋予了相应了初始值:

    instance = null , x = 0 , y = 0
    

    在类的初始化阶段,需要为每一个类赋予程序编写时期所期待的正确的初始值,首先会进入instance的构造函数中(构造函数在class中也有相应的函数名,init() 少了一个class前缀),执行完instance的构造函数后,各个静态变量值:

    instance = Singletion@4ffd399ed52,x=1,y=1
    

    然后,为x初始化,由于x显示地进行赋值,因此0才是所期待的正确赋值,而y由于没有给定初始值,在构造函数中计算所得的值就是所谓的正确赋值:

    instance = Singletion@4ffd399ed52,x=0,y=1
    

    首先会进入instance的构造函数中(构造函数在class中也有相应的函数名,init() 少了一个class前缀),执行完instance的构造函数后,各个静态变量值:

    instance = Singletion@4ffd399ed52,x=1,y=1
    

    然后,为x初始化,由于x显示地进行赋值,因此0才是所期待的正确赋值,而y由于没有给定初始值,在构造函数中计算所得的值就是所谓的正确赋值:

    instance = Singletion@4ffd399ed52,x=0,y=1
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值