JVM类的加载

引言

jvm执行引擎是jvm核心组成之一,相当于物理机中的cpu,然而它执行的前提是字节码文件被加载到虚拟机之内,类的加载就是执行的前提。

一个类的生命周期从它被加载到内存到被卸载出内存有7个阶段:加载(loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、卸载(Unloading)。其中前5个是属于类的加载(注意着加载和第一阶段是不同的含义)的过程。其中验证、准备、解析统称链接。

注意:以下文字涉及到的对象、初始化等名称都是指类的初始化,本博文就是在说明类的加载,而不是对象实例的初始化对象实例的加载。

类的加载

加载

和类加载不是一回事,加载的工作包括3件事,
1. 就是找到需要加载的类,获取定义这个类的二进制字节流
2. 并把类的信息加载到jvm的方法区中,
3. 然后虚拟机中实例化一个java.lang.Class对象,作为方法区中关于这个类的各种数据、信息的访问入口。

注意

这是类加载的第一阶段,对于它所做的3件事每个都能引申出很多,java虚拟机规范没有给出明确的规定:
1. 二进制字节流的来源,可以是网络(如applet),可以是class文件中的,也可以是非class文件中的,还可以生成(如jdk动态代理和CGLib)
2. 存储在方法区中是按照一定的格式存储的,这个格式是具体的虚拟机实现自行定义的
3. Class对象是属于对象的,但是它的具体存放位置没有规定,可以存放在堆中也可以不在堆中,对于被使用最多的HotSpot虚拟机而言,它就存放在方法区中。

连接

连接分3阶段

验证

验证是虚拟机对自身保护的一项重要工作。用来验证二进制字节流中的数据是否符合规范,是否会危害虚拟机自身。由于加载阶段获得的二进制字节流的来源很丰富,编译器的约束在这里已经失效了,从语法上不符合java规范的操作从某种意义上说都是可以实现的,所以验证变的很必要,但是这一步骤可以关闭,通过设置Xverify:none 来关闭大部分类验证措施。

准备

正式为类变量(static的)分配内存、设置类变量的初始值,这个初始值是指0,null,false等默认值。
我认为这一行为类似于“占座”,他们都是不依赖于对象实例存在的它们可以被分配内存了,但是总不能什么都不放吧,每一位上要么是0要么是1,那就尽量放0好了。而对于final的类变量,也就是final static的变量,更是在这个阶段直接付给了初始化值。至于static变量的初始化值,存放在类构造器<Clinit>()方法中。到初始化阶段才会调用。

解析

将符号引用替换为直接引用。
符号引用只是一个字符串,而不是具体的地址,解析完之后,转化为直接引用,其实就是转化为直接地址了。

注意
1. 符号引用可以理解为说明,这个时候这个被应用的类或者方法可以不存在于内存中,而且符号引用也没有说明它到底在哪,需要解析才知道,而直接引用就是一个直接地址(不管它是指针也好,偏移量也好),这个直接地址一旦出现,说明这个引用就必须在内存中,也就是已经完成了加载变成了内存中的class对象了
2. 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和电泳点限定符这7类进行。这些类型分类在java虚拟机规范中关于class文件的结构定义中提及,在《深入理解java虚拟机》第六章。

为何有这么一个过程呢?为何编译器不直接就完成这个步骤呢?这跟动态语言特性息息相关,想进一步了解需要明白——执行引擎动态调用。自己给自己留个坑,后续补充,留个地址链接的位置

初始化

这是类加载的最后一步,也只有到了这一步,被加载的类中的程序代码(字节码)才得以执行,之前都是虚拟机的代码在执行,是虚拟机起控制和主导作用的。(黑体字部分是有待商榷的,因为final static的字段其实在在准备阶段就执行了赋值语句,某种程度上算是执行了代码)

在准备阶段类变量已经赋了默认的0值null值,到了这个阶段,才会按照用户的主观意愿去赋值。或者说这一阶段是<Clinit>()方法的执行阶段。

<Clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作、静态代码块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中能访问到定义 在静态语句块中只能访问到定义在这个代码块之前的变量,定义在它之后的变量,只能在次代码块中赋值,而无法访问。如下面的代码所示。

至于为什么静态代码块可以赋值,而无法访问,这个应该是编译器规定的,跟虚拟机无关。可以这样解释,之所以可以赋值,是因为java的变量声明与赋值是跟书写顺序无关的,这一点在很早学c的时候就知道。而你要访问一个还没有被声明的变量则是非法的,由于涉及到作用域的问题,访问和声明是有顺序的。

public class Test{
    static{
        a = "md";//这里可以赋值
        System.out.print(a);//这里访问是非法的,无法通过编译
        //报错Cannot reference a field before it is defined
    }
    public static String a = null;
}
//需要补充说明的一点是,<clinit>方法的执行时按源码的书写顺序的,所以a最后的值是null,而不是md

上面的代码有些酸秀才咬文嚼字的意味了,但是理解这些还是非常重要的。

  1. <Clinit>()方法与<init>()类似都需要在子类的这个构造方法之前调用父类的方法,只不过<Clinit>()不需要显示的调用(也就是说不需要存在<init>()里面的super()这种)。
    由于父类方法首先执行,那么就意味着虚拟机中第一个被执行<Clinit>方法的类或者说第一个被初始化的类肯定是java.lang.Object。
  2. 接口也是存在<Clinit>()方法的,接口虽然没有静态代码块,单一样存在final static的变量,需要注意的是:接口的<Clinit>()方法执行并不需要父接口的<Clinit>()方法执行。原因显而易见,都是final static变量,只能赋值一次,而且在准备阶段都已经赋值了,根本不需要。
    那这里我就有了一个疑问,《深入理解java虚拟机第二版》在7.3.3章节中提到有ConstantValue属性的字段会在准备阶段直接赋予初值,而<Clinit>()方法对于类和接口而言并非必须的,如果是这样的话,接口类就根本不需要<Clinit>()方法了,因为它没有static代码块,所有的变量都是final static的也就是都有ConstantValue属性。

再说一个咬文嚼字的问题:

/*这个例子主要为了理解父类的“类构造器<clinit>”是父类先调用,而且包含了static代码块,至于为什么Parent和Son都声明为static的,是为了不让他们初始化*/
public class Parent{
    public static int A = 1;
    static{
        A=2;
    }
    public static void main(String[] args) {
        System.out.println(Sub.B);//输出2
    }
}
class Son extends Parent{
    public static int B = A;
}

小结

类的使用有主动引用和被动引用之分。主动引用时类需要初始化,而被动引用时,不会触发初始化。也就是说如果一个类就仅仅被“被动引用”了一下下,然后就没用了,那么它不会被初始化然后就被卸载了。

类通过加载、连接、初始化在正式在虚拟机里落了户,它结构才算完整,才能够被正常使用。但是这是正常情况下,java是准静态语言,存在一些动态特性,这个后续补充,再挖一坑

类的生命周期中加载、验证、准备、初始化、卸载的执行顺序是固定的,这些过程会依次开始(注意这里只是顺序开始,并非顺序结束,往往前一个过程还没结束,后一个就开始了),而解析有可能在初始化之前也可能在初始化之后。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值