类加载会经过:
加载,验证,准备,解析,初始化
等5个阶段,顺序是确定的(并发执行,不一定按顺序结束),而解析不确定,有些情况下可以在初始化之后进行。
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括7 个阶段:加载、验证、准备、解析、初始化、使用、卸载
jvm对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。
具体加载过程:
通过类的全限定名获取该类的二进制字节流
将二进制字节流所代表的静态结构转化为方法区的运行时数据结构
在内存中创建一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
装载过程主要是查找和导入class文件。在加载阶段完成之后,内存当中,运行时数据区的方法区以及堆就已经有数据了。
方法区:类信息,静态变量,常量
堆:代表被加载类的java.lang.Class对象
加载.class文件,得到二进制字节流,总结有如下方式:
从本地系统中直接加载
从zip包中读取加载.class文件,如 jar、war等
通过网络下载.class文件
从专有数据库中提取.class文件
通过动态代理技术生成代理类的二进制字节流
将Java源文件动态编译为.class文件,也就是运行时计算而成,即使用动态代理技术
从加密文件中获取,典型的防Class文件被反编译的保护措施
验证是为了确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,并且还要求信息不会危害虚拟机自身的安全,导致虚拟机的崩溃。
1.文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。这阶段的验证是基于二进制字节流进行的,只有经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面验证都是基于方法区的存储结构进行的。
例如:验证版本号是否正确
2.元数据验证
对类的元数据信息进行语义校验(其实就是对Java语法校验),保证不存在不符合Java语法规范的元数据信息。
例如:是否继承了final类?final类是不能被继承的,继承会出问题
3.字节码验证
进行数据流和控制流分析,确定程序语义是合法的、符合逻辑的。对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
例如:进行运行检查,栈空间数据类型和操作码操作参数是否吻合
4.符号引用验证
它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。符号引用验证的目的是确保解析动作能正常执行。
例如:常量池中描述类是否存在
添加参数:-Xverify:none
,取消验证
准备阶段是为类变量(静态变量)分配内存并且设置该类变量的默认初始值。进行分配内存的只是包括类变量(静态变量),而不包括实例变量,实例变量是在对象实例化时随着对象一起分配在java堆中的。
数据类型 | 值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | \u0000 |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
注意:
不包含用final修饰的static,final在编译的时候就会分配,准备阶段会显式初始化
不会为实例变量(没加static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
例如:
public static int a=1
,a在准备阶段过后的初始值为0,不为1,只是开辟内存空间,并没有运行java代码,a赋值为1的指令是程序被编译后,存放于类构造器()方法之中,所以a被赋值为1是在初始化阶段才会执行。
解析就是把类中的符号引用转换为直接引用,如果有了直接引用,那引用的目标必定存在内存中。
符号引用是一组符号来描述目标,可以是任何字面量。引用的目标并不一定已经加载到了内存中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行
对解析结果进行缓存
同一符号引用进行多次解析请求是很常见的,因此,虚拟机实现可以对第一次解析结果进行缓存,来避免解析动作重复进行。
初始化阶段是执行类构造器方法的过程。
在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据开发者通过程序制定的主观计划去初始化类变量和其他资源,比如赋值。
类变量进行初始值设定有两种方式:
声明类变量是指定初始值
使用静态代码块为类变量指定初始值
JVM初始化步骤:
如果类还没有被加载和连接,则程序先加载并连接该类
如果类的直接父类还没有被初始化,则先初始化其直接父类
如果类中有初始化语句,则系统依次执行这些初始化语句
当一个类在初始化时,要求其父类全部都已经初始化过,但是一个接口在初始化时,并不要求其父接口全部都完成初始化,当真正用到父接口的时候才会初始化。
使用是指初始化过程什么时候会被触发执行,即类的初始化时机。
1.主动引用
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种:
创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(如 Class.forName(“com.carl.Test”) )
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类( JvmCaseApplication ),直接使用 java.exe 命令来运行某个主类
2.被动引用
引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化
定义类数组,不会引起类的初始化
引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化的)
在类使用完之后,如果满足下面的情况,类就会被卸载:
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
注意:
Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。