简述:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类从被加载到内存中开始,到卸载出内存,经历了加载、连接、初始化、使用四个阶段,其中连接又包含了验证、准备、解析三个步骤。这些步骤总体上是按照图中顺序进行的,但是Java语言本身支持运行时绑定,所以解析阶段也可以是在初始化之后进行的。以上顺序都只是说开始的顺序,实际过程中是交叉进行的,加载过程中可能就已经开始验证了。
类加载的时机
- 当创建对象时(new)属于静态加载
- 当子类被加载时,父类也被加载 属于静态加载
- 调用类中的静态成员时 属于静态加载
- 通过反射 属于动态加载
静态加载:编译时加载相关的类,如果没有则报错,依赖性太强
动态加载:运行时加载需要的类,如果运行时不用该类,即使不存在也不报错,降低了依赖性
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
int result = scanner.nextInt();
System.out.println("输入的值"+result);
switch (result){
case 1:
Dog dog = new Dog(); //静态加载,依赖性很强
dog.cry();
System.out.println("输入的数值为1");
break;
case 2:
Class aClass = Class.forName("Person"); //加载Person类{动态加载}
Object o = aClass.newInstance();
Method hi = aClass.getMethod("hi");
hi.invoke(o);
break;
default:
System.out.println("do nothing");
}
}
结论:
因为new Dog()是静态加载,所以必须编写Dog类,要不然程序不能编译,会报错。
Class.forName("Person")是动态加载,只有执行该段代码的时候才会报错,编译的时候不会报错
类的加载过程
1.加载
加载分为3个阶段:
1、通过一个类的全限定名获取此类的二进制字节流,(JVM并没有规定字节流一定要用某种方式,也可以通过压缩包(jar、war包等)、从网络上获取、动态代理生成、其他文件(JSP)、数据库、加密文件(防反编译)等方式获取字节流)
2、将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
对于Class对象,Java虚拟机规范并没有规定是存储在Java堆中,HotSpot虚拟机将其存放在方法区内。
2.验证
1.目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
2.包括:文件格式验证(是否以魔数oxcafebabe开头)、元数据验证、字节码验证和符号引用验证
3.可以考虑使用-Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
文字格式验证: 该阶段主要在字节流转化为方法区中的运行时数据时,负责检查字节流是否符合Class文件的规范,保证其可以正确的被解析并存储于方法区中。
主要验证点:
- 是否以魔数
0xCAFEBABE
开头 - 主次版本号是否在当前虚拟机处理范围之内
- 常量池的常量是否有不被支持的类型 (检查常量tag标志)
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
- Class文件中各个部分及文件本身是否有被删除的或者附加的其他信息
元数据验证:该阶段负责分析存储于方法区的结构是否符合Java语言规范的要求
主要验证点:
- 该类是否有父类(只有Object对象没有父类,其余都有)
- 该类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,出现不符合规则的方法重载,例如方法参数都一致,但是返回值类型却不同)
字节码验证:该阶段则负责分析数据流和控制流,确定方法体的合法性,保证被校验的方法在运行时不会危害虚拟机的运行。
主要有:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似的情况:操作数栈里的一个int数据,但是使用时却当做long类型加载到本地变量中
- 保证跳转不会跳到方法体以外的字节码指令上
- 保证方法体内的类型转换是合法的。例如子类赋值给父类是合法的,但是父类赋值给子类或者其它毫无继承关系的类型,则是不合法的。
符号引用验证:最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段解析阶段发生。符号引用是对类自身以外(常量池中的各种符号引用)的信息进行匹配校验。
主要有:
- 符号引用中通过字符串描述的全限定名是否找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、方法、字段的访问性(private,public,protected、default)是否可被当前类访问
符号引用验证的目的是确保解析动作能够正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
验证阶段非常重要,但不一定必要,如果所有代码极影被反复使用和验证过,那么可以通过虚拟机参数-Xverify: none
来关闭验证,加速类加载时间。
3.准备阶段
JVM会在该阶段对静态变量,分配内存并默认初始化(对应数据类型的默认初始化值,如0,0L,null,false等),这些变量所使用的的内存都将在方法区中进行分配。
看代码注释
class A {
//类的加载过程连接阶段-准备阶段 属性是如何处理的
//1.n1 是实例属性,不是静态变量,因此在准备阶段,是不会分配内存的
//2.n2 是静态变量,给n2分配内存,默认初始化值为0,而不是20。 20是类的加载过程中最后一阶段-初始化才会给n2赋值为20
//3.n3 是常量,他和静态变量不一样,因为他一旦赋值就不变了 n3=30
public int n1 = 10;
public static int n2 = 20;
public static final int n3 = 30;
}
4.解析阶段
解析阶段将常量池中的符号引用替换为直接引用。
在字节码文件中,类、接口、字段、方法等类型都是由一组符号来表示,其形式由Java虚拟机规范中的Class文件格式定义。在虚拟机执行特定指令之前,需要将符号引用转化为目标的指针、相对偏移量或者句柄,这样可以通过此类直接引用在内存中定位调用的具体位置。具体详情百度就可以,百度有很多教程
5.初始化阶段
1.初始化是类加载的最后一步,在前面的阶段里,除了加载阶段可以通过用户自定义的类加载器加载,其余部分基本都是由虚拟机主导的。但是到了初始化阶段,才开始真正执行用户编写的java代码了。
2. <clinit>()方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。
3.虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法。
public class Test1 {
public static void main(String[] args) {
//1.加载B类,生成class对象
//2.连接阶段过后 num = 0(连接阶段的准备阶段会给静态变量赋默认值)
//3.初始化阶段
// 依次收集类中所有的静态变量的赋值动作和静态代码块中的语句 并合并
/**
* 收集
* static {
* System.out.println("静态代码块执行");
* int num = 30;
* int num = 10;
* }
*
* 合并
* static {
* System.out.println("静态代码块执行");
* int num = 10;
* }
*
* 得到结果为10
*/
System.out.println(B.num);
}
}
class B{
static {
System.out.println("静态代码块执行");
int num = 30;
}
public static int num = 10;
}
总结:
正确掌握类加载的过程,可以对平常编程中的各种问题有更深入的了解。下面通过一个违背感觉的实例,来感受下掌握类加载过程的重要性。
public class Test {
public static void main(String[] args) {
//1. 调用getInstance时,会进行SingleTon的类加载过程。创建Class对象
//2.连接阶段 验证阶段后到 准备阶段 就会赋默认值 singleTon=null count1=0,count2=0,然后在解析阶段
//3.最后初始化阶段 合并代码
/**
* clinit<>{
* SingleTon singleTon = new SingleTon();
* int count1 ;
* int count2 = 0;
* }
* 先执行new SingleTon(),此时count1=count2=1
* 然后由于count1无赋值操作,所以count1=1。count2赋值为0,
*
*/
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
}
假如将private static SingleTon singleTon = new SingleTon();语句移动到
public static int count2 = 0;**之后,.
public class Test {
public static void main(String[] args) {
//1.加载阶段,创建Class对象
//2.连接阶段 验证阶段后到 准备阶段 就会赋默认值 singleTon=null count1=0,count2=0,然后在解析阶段
//3.最后初始化阶段 合并代码
/**
* clinit<>{
* int count1 ;
* int count2 = 0;
* SingleTon singleTon = new SingleTon();
* }
* 先执行count1和count2 在准备阶段就给 count1=0,count2=0
* 然后再执行new SingleTon(),此时count1和count2自增1 所以count1=count2=1
*/
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
class SingleTon {
public static int count1;
public static int count2 = 0;
private static SingleTon singleTon = new SingleTon();
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
}
参考链接:
链接:https://blog.csdn.net/weixin_39538847/article/details/111365670
链接:https://juejin.cn/post/6844903517384081416