简介
当程序主动使用某个类时,如果该类还未被加载到内存,才会对其进行加载,JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
- 懒加载机制:需要用的时候,才会加载,而不是在程序运行的开始就把所有类加载了
- 节省资源
- 首次打开速度稍慢
- 在java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的
类加载注意事项
- 类加载器并不需要等待某个类被“首次主动使用”时再加载它
- JVM规范允许类加载器在预料到某个类要被使用时就预先加载它
- 如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才会报告错误(LinkageError错误),如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
类加载时机
类的生命周期
由图可知,类从被加载到内存开始,到卸载出内存为止,整个生命周期包括七个阶段,而验证、准备、解析3个阶段统称为连接。
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是固定的,类的加载过程必须按照这种顺序开始。
解析阶段则不一定,它可以在某些情况下在初始化后才开始。
类加载过程
加载
- 通过一个类的全限定名获取定义此类的二进制流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆区创建一个java.lang.class对象
- 封装了类在方法区内的数据
- 向Java程序员提供了访问方法区内的数据结构的接口
- 简单来说就是作为这个类的各种数据的访问入口
注意:
Class对象是存放在堆区的,不是方法区,这点很多人容易犯错。类的元数据才是存在方法区的。【元数据并不是类的Class对象。Class对象是加载的最终产品,存放在堆中,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的】
Class对象与new出来的对象之间的关系
new出来的对象以car为例。可以把car的Class类看成具体的一个人,而new car则是人物映像,具体的一个人(Class)是唯一的,人物映像(new car)是多个的。镜子中的每个人物映像都是根据具体的人映造出来的,也就是说每个new出来的对象都是以Class类为模板参照出来的!为啥可以参照捏?因为Class对象提供了访问方法区内的数据结构的接口。
加载阶段总结:
.class文件(二进制数据) ——> 读取到内存 ——> 数据放进方法区 ——> 堆中创建对应Class对象 ——> 提供访问方法区的接口
验证
主要是为了确保Class字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 文件格式验证:确保是class文件
- 元数据验证:保证数据没有危害
- 字节码验证:确保程序的语言符合规则
- 符号引用验证:保证引用解析动作能正确执行
准备
当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
内存分配的对象:
- 类变量:指的是被static修饰的变量
- 类成员变量:除了类变量之外的其他所有类型的变量
注意,在准备阶段,JVM只会为类变量分配内存。 类成员变量的内存分配需要等待初始化阶段才能开始
初始化:
- 由于只会为类变量分配内存,所以当然只会初始化类变量; 类成员变量不会在这个时候初始化
- 初始化指的是为变量赋予Java语言中该数据类型的默认值,而不是用户代码中初始化的值。
示例:这段代码在准备阶段之后,exam的值将是0,而不是666;1
public static int exam = 666;
注意
注意
注意
但如果一个变量是常量(被static final修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。此时的代码在准备阶段之后,exam的值就是666了。1
public static final int exam = 666;
解析
解析阶段是虚拟机将常量池中的符号引用替换成直接引用的过程。一个符号引用多次解析后,就会将解析的结果进行缓存,避免多次解析浪费资源
- 符号引用:以一组符号来描述引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可
- 直接引用:
- 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
- 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
- 一个能间接定位到目标的句柄
初始化
- 初始化,是类加载的最后一步
- 初始化时执行
<clinit>
方法(class init)的过程
到了初始化阶段,用户定义的Java程序代码才真正开始执行。
Java程序对类的使用分为两种方式:
- 主动使用
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰不不不其实更准确的说是在编译期把结果放入常量池的静态字段除外)
- 调用类的静态方法
- 反射
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类,还有就是main方法的类会首先被初始化
- 被动使用:除了上述六种情况外,使用类的方式都看作是对类的被动使用
初始化过程:
- 类初始化方法:编译器会按照其出现顺序,收集:类变量(static变量)的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。
- 对象初始化方法:编译器会按照其出现顺序,收集:成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法,值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。
注意,静态类变量,代码块,只会在首次主动调用类的时候进行初始化,后面调用类,是不会再执行静态代码块的。
使用与卸载
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。
在以下几种情况,Java虚拟机将结束生命周期:
- 执行System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止。
接口的加载
接口的加载与类加载过程有所不同。
当一个类在初始化时,要求其父类全部都已经初始化过了。但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。
理解首次使用
- main方法的类会首先被初始化,如果有父类,先初始化父类
- 如果main方法的类还有静态代码块,先初始化静态代码块,再初始化main方法
- 定义一个对象,如
Fath fath
; 这只是声明了一个引用,不会执行什么 - 当运行到
Fath fath = new Fath()
;看到关键字new,并且将引用fath指向了Fath对象,就说明主动使用了,所以Fath类会被初始化- 当后面再使用 new Fath()时,由于不再是首次使用,就不会再初始化Fath类了
参考
- 本文作者: xczll
- 本文链接: https://xczllgit.github.io/2020/03/29/jvm/2020-03-29-classLoaderMechanisms/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!