原创 | jvm04、我所知道的类加载
小白带你快速了解
类的生命周期:
我们需要熟悉类加载过程:
- 加载(将类的二进制数据加载到内存)
- 验证(确保加载类的正确性)
- 准备(为类的静态变量分配内存,设置默认值)
- 解析(符号引用转换为直接引用)
- 初始化(设置类的正确初始化值,jvm初始化类)
准备阶段还真关系到我们日常编码的一个注意点呢!是个面试题也不为过!
庆哥陪你深入分析
对于什么是类我们比较清楚,那什么是类的加载呢?java程序是运行在内存中的,而类的加载就是将类的.class文件中的二进制数据存放在内存中,这个内存指的是jvm内存。方法区中有一个运行时常量池就是生成的class文件中的class文件常量池进入内存之后的版本。
类加载的最终结果是在堆中创建一个java.lang.Class文件。这个加载进内存的.class文件就是我们将java源文件动态编译得到的,也就是javac命令。
首先来看类的生命周期
类的生命周期一共七个步骤,其中加载,验证,准备,解析和初始化时类加载过程,验证和准备还有解析三个阶段也被叫做连接阶段。
这里有一个需要注意的就是加载,验证,准备和初始化这四个阶段的顺序是确定的,但是解析这一阶段就不一定了,它也有可能在初始化之后才开始,这是为了支持java的运行时绑定,另外以上这几个阶段是按顺序开始,但是可没有说按顺序结束,也就是他们一般情况下都是混杂着进行的。
加载阶段
加载阶段是类的生命周期最开始的步骤,这一阶段的目的主要在于将类的二进制数据加载到内存,一般都是通过类的全限定名称来获取二进制字节流,然后将这个字节流代表的静态存储机构转换为方法区中的运行时数据结构,我们知道类加载的最终结果是在java堆中产生一个Class对象,那么这个对象是用来干嘛的呢?它就是用于后续对方法区这些数据进行访问的一个结构,也就是我们可以通过这个类来访问到方法区中的这些数据。
这个加载过程一般有这么两种方式
- 使用系统已经为我们提供好的类加载器来进行加载
- 使用自定义的类加载器来完成加载
连接阶段
这个连接阶段包括验证,准备和解析
验证
验证这一步骤的主要目的就是为了确保加载的类的正确性,以防加载对虚拟机有危害的类。这样的话对安全的类就有一个评判标准,一般有如下验证步骤
• 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
• 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
• 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
• 符号引用验证:确保解析动作能正确执行。
另外一点需要注意的是,验证这一阶段是非常重要的,是用来保证虚拟机的安全,但是这一阶段却不是必须的,什么意思呢?当你确定你这个类是安全的,也就是经过反复验证符合虚拟机规范的话,就可以考虑采用Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
准备阶段的主要目的是为类的静态变量分配内存,并将其设置默认值。
这一阶段会在方法区为类变量进行默认值赋值,但是这个类变量只是静态变量,而不是实例变量,实例变量会在对象实例化的时候跟随对象一块分配在java堆中。
这里指的默认值通常情况下基本数据类型就是默认的零值,像0,null和false等。
好好说说这个准备阶段
这个准备阶段是可以好好研究下的,在说这个准备阶段的时候,我们先来看两个关键字,那就是static和final,这个一个是代表静态,一个是代表常量,上面说过了,在准备阶段的主要目的就是给这些静态变量分配内存,设置初始默认值,这个是什么意思呢?我们拿代码来举例子
看下面的代码
public static int age = 2
以上代码就是简单定义了一个int变量,并且赋值成2,这个是在我们写代码的层面来说的,但是深入jvm,也就是类加载的是时候又是怎么回事呢?这个主要集中在类加载过程中的准备阶段,在准备阶段的时候,这个age其实不等于2,而是等于0,因为在准备阶段会为静态变量赋初始值,那什么时候才是等于2呢?这个是在后面的初始化阶段。
这里就又值得说道说道了,要知道,只有静态变量在这个准备阶段才会被赋初始值,其他的都靠边站了,所以这里就有个知识点了,就是局部变量和全局变量以及静态变量也就是static修饰的变量,记住了
如果是基本数据类型,在准备阶段会为静态变量和全局变量赋初始值,也就是说如果你没有给他们显示的赋值就直接使用的话,系统会为他们赋初始值,也就是默认值,这个是在准备阶段完成的,但是对于局部变量就不一样了,如果你要使用局部变量的话,那必须在使用之前就给它赋值,否则编译你都通过不了,还是举个例子吧
看这个例子,a是一个静态变量,b是一个全局变量,这些都可以实现不为其赋值,但是人家可以直接使用,因为在准备阶段它们会被设置默认值0,但是这个局部变量c就不一样了,如果你也不赋值,那结果是你编译都过不了。
解析阶段
在解析阶段最主要的目的就是把类中的符号引用转换为直接引用。
另外要知道的是这个符号引用是怎么回事,当然还有和这个直接引用,知道了什么是符号引用和直接引用,那么这个解析阶段也就ok了
符号引用 :强调的是编译成class文件之后,这个时候并不能确定一个类的引用到底指向谁,因此只能使用特定的符号代替,这就叫做符号引用,比如在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。
直接引用:在类加载阶段,经过解析将符号引用解析成直接引用,也就成了指向一个具体目标的内存地址。
初始化阶段
在这个初始化阶段就是将类的静态变量赋予正确的值了,也就是你想要它表示的值,也就是这个
public static int age = 2
你这个在准备阶段给我搞个默认值0,但是我想让他等于2啊,所以在这个初始化阶段就给它设置成2了,不过在这个初始化阶段可不单单是给类的静态变量初始化正确的值,在这个阶段jvm还会对类进行初始化。
对类进行初始化?是的,这个主要就是对类的变量进行初始化,注意这里可不只是静态变量,另外还有一点就是类在什么时候才会被初始化呢?这个就是在类被主动使用的时候才会导致类的初始化,以下几种情况都会导致类的主动使用。
- 使用new来创建一个实例对象
- 访问了类或者接口的静态变量,或者你对静态变量赋值
- 使用类的静态方法
- 你使用了反射
- 等等
剩下的使用和卸载也就不用说了,任性!