一、概述
生命周期的7个阶段:
从使用过程看:
最后会在方法区,存在类的模版,之后就可以使用这个类了。
二、过程1:Loading阶段(加载)
所谓加载,就是将字节码文件加载到机器内存中,并在内存中构建出Java类的原型-类模版对象,其是一个java类在JVM内存中的一个快照,JVM将字节码文件中解析出来的常量池、类字段、类方法都存储到模版中,这样JVM在运行期间可以通过类模版获取到类中的任意信息、遍历成员变量、方法调用。反射机制也基于这个。
1、加载完成的操作
查找并加载二进制数据,生成class的实例。
在加载类的时候,JVM必须完成一下三件事:
- 通过类的全名,获取类的二进制数据流
- 解析类的二进制数据流为方法区内的数据接口(java类模型)
- 创建java.lang.Class类的实例,表示该类型。作为方法区这个类各种操作的入口
2、二进制流的获取方式
- 文件系统读取
- 读取jar、zip包
- 数据库二进制
- 使用网络方式传输二进制流
- 在运行期间动态生成
只要符合JVM规范即可,读取后在内存中生成该类的java.lang.Class的实例。
3、类模型与Class实例的位置
1、类的存放位置就是方法区(JDK7之前是永久代、JDK8之后是元空间)
2、Class实例的位置就是堆中,class文件加载到方法区之后,就会在堆中创建对应类的Class类型对象
4、数组类的加载
创建数组类的情况有些特殊,因为数组类并不是有类加载器创建的,而是由JVM在运行时候根据需要直接创建的,但数组内的元素类型仍然要类加载器区创建,过程如下:
- 如果数据元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建其元素类型
- JVM使用指定的元素类型和数组维度来创建新的数组类
如果数组类型的元素是引用类型,其访问性就是由元素类型的访问性决定的。否则都是public。
三、过程2:Linking阶段(连接)
1、环节1:Verification(验证)
主要是保证加载字节码是合法、合理并符合JVM规范。
整体说明:
- 格式检查,是loading过程中就进行,对class文件的格式进行检查
- 语义检查,保证字节码在语义上是符合规范的
- 字节码检查,保证二机制文件的字节码是符合规范的
- 符号引用检查,在解析环境才执行,检查类和方法是否存在
2、环节2:Preparation(准备)
该环节为类的静态变量分配内存,并将其初始化为默认值。
注意:
- 基本数据类型和string通过字面量的方式,都是在准被环节进行显示赋值
- 这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量会随着对象一起分配的堆中
- 这个阶段不会想初始化阶段那样会有初始化或者代码被执行,仅仅是赋值
- 基本数据类型 static final 会在准备环境赋值
3、环节3:Resolution(解析)
将类、接口中的字段、方法从符号引用转换为直接引用。
符号引用就是一些字面量的引用,和虚拟机加载什么类无关。所以当实际运行的时候,需要把符号引用中引用的相关类、接口加载到内存中,也就是转换成直接饮用。
通常是在初始化之后(Initialzation)的进行的,字符串常量池中不存在重复项。
四、过程3:Initialzation阶段(初始化)
该阶段是类装在的最后阶段,主要是为类的静态变量赋予正确的初始值。其重要的工作是执行<clinit>()方法:
- 该方法仅能又java编译器生成并又JVM调用,开发者无法定义同名的方法,也无法调用
- 它是由类静态成员变量的肤质语句以及static代码块合并产生的
在加载一个类之前,虚拟机总会试图加载其父类,因此父类的<clinit>()总在子类的<clinit>()之前调用,也就是说父类的static块优先级高于子类(所以值是子类的)。
那么哪些不会包含<clinit>()方法呢?
- 没有生成声明任何static变量,也没有静态代码块
- 声明staitc变量,但是没有明确使类变量的初始化语句以及静态代码块类执行初始化操作
- 包含static final 修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式(final)
1、static和final搭配的问题
- 在链接阶段的准备环节:
- static+final修饰的基本数据类型
- static+final修饰的string字面量显示赋值
- 在初始化<clinit>()环节:
- 只有static修饰的基本数据类型、引用数据类型
- static+final修饰的引用数据类型
- static+final修饰的字符串string,new String
最终结论,使用static+final修饰且显示赋值中不涉及到方法或构造器调用的基本数据类型或string类型的显示赋值,是在链接阶段的准备环境进行的。
2、<clinit>()线程安全性
因为<clinit>()已经被加锁了,所以在一个线程已经开始执行<clinit>()的时候,其它线程时没办法执行的。并且这个锁是隐式的锁。如果一个类的<clinit>()耗时很长,那么可能存在线程阻塞,引发死锁。并且这种情况不好排查。
3、类的初始化情况:主动使用VS被动使用
主动使用类会调用<clinit>(),被动使用类不会调用<clinit>()
1、主动使用
Class只有在必须要首次使用的时候才会被装载,Java虚拟机不会无条件的装载Class类型。JVM规定,一个类或接口在初次使用的时候必须要进行初始化。这里指的使用,就是主动使用,情况如下:
- 创建一个类的实例时候,new、反射、序列话、克隆
- 调用静态方法,即使用了字节码invokestatic指令
- 当使用类、接口的静态字段时候
- 当使用java.lang.reflect包中的方法反射类的方法时
- 当初始化子类时,如果发现其父类还没有进行初始化过程,先出发父类初始化
- 如果一个接口定义了default方法,那么直接或间接实现该接口类的初始化,会先出发该接口的初始化
- 当JVM启动时候,用户定一个要执行的主类(main方法),JVM会先初始化这个主类
- 当初次调用MethodHandle实例的时候,初始化MethodHandle指向方法所在了
2、被动使用
处理以上的情况,其它都属于被动使用,不会引起类的初始化,也就是说代码中出现的类如果不符合情况,不会被初始化:
- 当访问一个静态字段是,只有真正声明这个字段的类才被初始化(通过子类调用父类的静态变量,子类不会初始化)
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类或接口的初始化,因为常量在链接阶段就已经显示的被赋值了
- 调用ClassLoader类的loadClass()方法加载一个类,并不是对类主动使用,不会被初始化
主动使用类会被初始化,被动使用类不会被初始化。
五、过程4:类的Using(使用)
前面的环境已经保证类可以使用了。开发者可以在程序中调用类的静态方法、静态成员变量了。
六、过程5:累的Unloading(卸载)
1、类、类的加载器、类的实例之间的引用关系
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用,另一方面,一个class对象总是会引用它的类加载器,调用class对象的getClassLoader()方法,就能获取类的加载器。所以class实例和类的加载器是双向关联的关系。
一个类的实例总是引用代表这个类的class对象。在Object类中定义了getClass()方法,这个方法返回对象所属类的Class对象的引用。此外,所有java类都有一个静态属性class,它引用代表这个类的class对象。
2、类的生命周期
当一个类被加载、连接、初始化之后,它的生命周期就开始了。当类的Class对象不在被引用,即不可触及的时候,class对象就结束生命周期,类在方法区也会被卸载。一个类合适结束生命周期,取决于class对象何时结束生命周期。
- 该类所有对象、子类对象都被回收
- 类的加载器被回收了(很难)
- Class对象也没有在任何地方被引用
满足上诉三个情况才能允许类卸载。启动类无法被卸载、扩展类加载器和系统类加载器都和很难被卸载,因为都会直接或间接的被用到。
只有自己开发的类加载器,才可能被卸载。