在Java语言中,类型的加载、链接和初始化等动作都是在程序允许期间才开始。
缺点:编译更麻烦,加载时性能开销更大
有点:扩展性强,灵活性强
一个类从被加载到虚拟机内存再到卸载出内存,它的整个生命周期将会经历以下七个阶段
各个步骤之间按顺序进行,但不是上一个结束下一个才开始,通常是上一个在执行时下一个也开始执行了(解析阶段除外,解析阶段可以在初始化之后才做,这是为了支持Java语言的动态绑定性)
Java没有对一个类什么时候加载做出规范。但Java规范了什么时候虚拟机需要对一个类进行初始化
而初始化自然JVM需要对类进行加载链接等步骤
六种一定要初始化的情况
Class文件中遇到new、getstatic、putstatic、invokestatice四条字节码指令
new关键字实例化对象
读取或修改某个类中的静态字段(static修饰的字段)注意:是非final修饰的字段
调用一个类的静态方法
使用反射对方法进行调用
初始化一个类时会连带着将其父类进行初始化
main方法所在的类会被Jvm先加载
新特性句柄对应的类型
当一个接口中定义了default方法后,它的某实现类被初始化后,接口将会被初始化(普通接口一般不会随着实现类的初始化而初始化)
注:这六种情况称为主动引用,主动引用会触发类的初始化
不会触发类的初始化的情况
加载阶段虚拟机主要完成三件事
类文件被加载进虚拟机时,需要通过类加载器去完成(一个类和类加载器有唯一确定关系),这个类加载器也可以由用户自定义加载方式
加载数组类
方法区中的存储结构
Java没有对虚拟机中方法区到底以何种形式的数据存储结构来保存类的信息。各虚拟机自行定义
主要工作:验证Class文件中的信息是否符合规范
验证阶段主要分为以下四个检验动作
文件格式验证
验证字节流是否符合class文件的规范比如
这一步主要是确保Claas文件流中的信息能正确读到方法区中,因为后续的验证都是基于方法区进行的,不再直接读取流
元数据验证
验证字节码中描述的信息是否符合Java语言规范
例如检验
这个类是否有父类(Java中除Object类以外都有父类)
这个类的是否继承了不被允许继承的类
是否是抽象类,是否符合抽象类的定义
类中的字段、方法是否和父类发生冲突,方法重载是否正确等
…
字节码验证
第二阶段对类中的元数据进行验证后
第三阶段将对类中的方法进行验证即Class文件中的code属性
即使虚拟机对字节码进行再多的验证都不能保证代码运行时能在有限的时间内结束
所以从JDK1.6开始,通过javac编译器和Java虚拟机进行联合优化。具体做法:在方法体Code属性中添加一个名为“StackMapTable”属性,由编译器在编译时来进行类型检查,虚拟机在进行字节码验证时只需要查看StackMapTable属性即可。
符号引用验证
发生在符号引用转化为直接引用时(解析阶段),检查对应的类是否缺少,或者禁止外部访问
主要检验类容
当项目中使用的代码都被多次验证和使用过,可以使用 -Xverify:none来关闭大部分的验证措施,减少虚拟机加载类的时间
为类变量赋值(类中的静态变量,static修饰的变量)
赋值为默认值,注意此处的赋值不会按代码中的赋值来进行赋值,而是赋对应类型的默认值,例如
class{
public static int i=10;
}
这个阶段只是会将i赋值为0,而不是10。(常量则直接赋值为初始值)
真正的赋值阶段推迟到初始化阶段
几种类型的默认值
引用数据类型默认值为null
主要作用:将JVM常量池中的符号引用替换成直接引用
符号引用
Class文件中的CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info等常量
可以理解为代码中类的类名,字段名,方法名等
直接引用
直接指向目标的指针或者间接定位到目标的句柄。如果直接引用存在,那引用目标也一定已经加载到了虚拟机中
解析时间:解析时机是不固定的,由各虚拟机自行决定,可以在类被加载器加载时即进行常量池的解析也可以在符号引用在被使用时进行。
能触发解析的指令有17个:checkcast、getfield、invokedynamic…对于除invokedynamic以外的指令触发解析时,不论解析多少次其解析结果将完全一致,不论成功失败
常见的几种解析
类的初始化是这个类加载的最后一个阶段。这个阶段与前面几个阶段主导权在JVM中不同,这个阶段由应用程序决定
本质就是执行编译器编译后生成的类构造器
类构造器是由编译阶段,编译器收集类中类变量的赋值动作和静态代码块的语句合并产生的
收集顺序由源文件中出现的顺序决定。所以如果在静态代码块后声明的静态变量,只能在静态代码块进行赋值但不能访问
类构造器并不是每个类都必须的,当类中没有静态代码块或者没有为静态变量赋值的动作时将不会生成类构造器
**注意:**类构造器只会执行一次
类的唯一性是由类和它对于的类加载器共同确立的
即使是来源于统一类文件的对象,它们的加载器不同时,对象属于的类也不相同
示例
package com.sy.offer;
import java.io.IOException;
import java.io.InputStream;
/**
* @author 沈洋 邮箱:[email protected]
* @create 2021/7/18-11:26
**/
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader myLoader = new ClassLoader(){
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is==null) return super.loadClass(name);
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.sy.offer.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof ClassLoaderTest);
}
}
输出:
class com.sy.offer.ClassLoaderTest
false
这里返回false的原因就是JVM中存在两个这个类的类加载器,一个是我们自定义的一个是JVM自己产生的
Java的类加载器主要是三层结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PVIt0BAM-1627275309749)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20210718152502148.png)]
** 下面都是针对Hotspot虚拟机讨论
启动类加载器
这个加载器复制加载
扩展类加载器
这个类加载器复制加载
应用程序类加载器
负责加载用户类路径上所有的类库
双亲委派的工作流程
一个类加载器收到加载类的请求后,会先将加载请求委派给其父加载器,每层加载器都是如此,直到传递到最上层加载器(启动类加载器),只有当父加载器无法加载时,才会重新交还给子加载器进行加载。这也保证了Object、String类等常用类无法被用户自定义覆盖。保证了系统安全性