JAVA(1) - 类的加载过程
- 前言
- 概念
- 1、类加载简介
- 加载
- 连接
- 初始化
- 类的主动使用与被动使用
- 主动使用
- 被动使用
- 2、加载阶段
- 3、连接阶段
- 3.1验证
- 3.2准备
- 3.3解析
- 在常量池中寻找类、接口、字段、和方法的符号引用(助记符),并将这些符号引用转换为直接引用的过程。
- 4、类的初始化阶段
- 4.1 `()`方法
- 5、输出
前言
类加载流程
扩展:
概念
- 符号/直接引用:能否直接使用
#https://www.zhihu.com/question/30300585/answer/51335493
带有类型(tag) / 结构(符号间引用层次)的字符串。
java/lang/Object."<init>":()V
直接引用
methodblock*
符号引用通常是设计字符串的——用文本形式来表示引用关系。而直接引用是JVM(或其它运行时环境)所能直接使用的形式。
它既可以表现为直接指针(如上面常量池项#2解析为methodblock*),
也可能是其它形式(例如invokevirtual_quick指令里的vtable index)。关键点不在于形式是否为“直接指针”,而是在于JVM是否能“直接使用”这种形式的数据。
1、类加载简介
- JVM对类的加载是延迟加载的,lazy、一个类在首次主动使用时才会被初始化,在同一个运行时包下面(运行时包与类包概念不一样),一个类只会被初始化一次
加载
- 查找和加载,将类的二进制文件即class文件加载到JVM中,dubbo通过SPI机制(Service Provider Interface)运行中创建类
连接
- 分为三点
- 验证:确保类的正确性,验证魔术因子是否为0xCAFEBAEE、版本号等
- 准备:将类的静态变量分配内存,并设置默认值(类型的默认值,非代码值)
- 解析:符号引用->直接引用 (标识转为栈内存到堆内存的对象引用,堆区对方法区的数据结构(运行时常量池)引用)
初始化
- 为类的静态变量赋予正确的初始化值(代码值)
类的主动使用与被动使用
主动使用
- new Object(),会导致类的加载与最终初始化
- 访问类的静态变量,包括读取与更新会导致类的初始化
- 调用静态方法
- 对某个类进行反射操作
Class.forName("com.today.roc.go.understand.thread.高并发编程详解.类的加载过程.类的主动使用与被动使用.SimpleChild");
- 初始化子类会导致父类的初始化(但是通过子类调用父类的静态变量只会导致父类初始化,子类不会初始化)
- 启动类,main函数所在类
//初始化
//System.out.println("访问静态变量:"+Simple.x);
Simple.x = 1000;//此时会初始化类,但是输出值为0还没有赋值
System.out.println(Simple.x);//输出1000
//Simple.test();
//System.out.println(SimpleChild.y);
//只会导致父类初始化,子类不会初始化
//System.out.println(SimpleChild.x);
//反射会导致初始化
//Class.forName("com.today.roc.go.understand.thread.高并发编程详解.类的加载过程.类的主动使用与被动使用.SimpleChild");
- 除了上述六种主动使用方式,其它都为被动使用
被动使用
- 创建对象数组
Simple[] simples = new Simple[10];
只是开辟出了一段连续的内存地址4byte * 10 - 引用类的静态常量不会导致类的初始化,赋值对象的静态常量值会导致类初始化,因为需要类初始化之后才能确定引用值
public class PassiveSimple {
static {
System.out.println("PassiveSimple will be initialized");
}
public static final String a = "123";
public static final String b = "123".toString();
}
//不加载 固定常量值的静态常量不会导致类初始化
System.out.println(PassiveSimple.a);
//加载 对象常量值或进行引用的静态常量会导致类初始化
System.out.println(PassiveSimple.b);
2、加载阶段
- 类的加载时通过一个全限定名(包名+类名)来获取二进制流、以下加载方式也可
- 运行时动态生成,ASM包 动态代理之类
- 网络获取,Applet小程序,RMI动态发布等
- 读取ZIP文件获取类的二进制流,比如jar、war(使用和zip一样的压缩算法)
- 类的二进制数据存储在数据库的BLOB字段类型中
- 运行时生成class文件,然后再加载
- 虚拟机会将二进制流按照虚拟机定义的格式储存在方法区中,形成特定的数据结构,随之在堆内存中实例化一个java.lang.Class对象。
3、连接阶段
3.1验证
- 验证文件格式
- 魔术因子,判断文件类型,class文件是0xCAFEBABE
- 主次版本号,jdk不同版本的处理支持不一样
- 构成class文件的字节流是否存在残缺
- 常量池常量是否存在不被支持的变量类型
- 指向常量的引用是否指到了不存在的常量或常量类型不支持
- 其他信息
- 元数据的验证
- 检查是否存在父类、是否继承某个接口,且父类接口是否存在和合法
- 检查是否被final修饰的类,不允许被继承和重写
- 是否抽象类,不是抽象类是否实现抽象父类的方法,以及接口的方法
- 检查重载的合法性,相同方法名称和参数,返回值不同,是不允许的
- 其他语义验证
- 字节码验证
主要验证程序的控制流程,比如循环,分支等
- 保证当前线程的程序计数器中的指令不会跳到不合法的字节码指令中去。
- 保证类型转换合法,比如A声明的引用,不能用B进行强转
- 保证任意时刻,虚拟机栈中的操作栈类型与指令代码都能正常运行,比如压栈是A类型的引用,实际将B类型载入本地变量表则会报错
- 其它验证
- 符号引用验证:验证符号引用转换为直接引用的合法性
- 通过符号引用描述的字符串全限定名称是否能顺利的找到相关的类
- 符号引用中的类、字段、方法、是否对当前类可见,比如不能访问引用类的私有方法
- 其他
3.2准备
- 为静态变量分配内存到方法区并设置默认值
3.3解析
在常量池中寻找类、接口、字段、和方法的符号引用(助记符),并将这些符号引用转换为直接引用的过程。
- 虚拟机规范规定,anewarray、checkcast、getfield、getstatic、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield、putstatic这13个操作符引用的字节码指令之前,必须对所有的符号提前进行解析。
- 解析的过程主要是针对类接口、字段、类方法、和接口方法这四类进行的,分别对应到常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_IntefaceMethodref_info这四种类型常量。
- 类接口解析
- 如果Simple不是一个数组类型,则需要先完成对Simple类的加载
- 如果是一个数组类型、不需要完成对Simple类的加载,生成一个代表该类型的数组对象,并在堆内存中开辟一片连续的地址空间即可
- 在类接口的解析完成后,还需要进行符号引用验证
- 字段的解析
- 如果Simple类本身包含某个字段,则直接返回这个字段的引用,当然也要对该字段所属的类进行提前加载
- 如果Simple中不存在该字段,则会根据继承关系自下而上,查找父类或者接口的字段,找到即可返回,同样需要对找到的字段进行类的加载过程
- 如果Simple中没有字段,并且找到最上层的java.lang.Object还是没有,则表示查找失败,也就不再进行任何解析,直接抛出NoSuchFieldError异常
- 类方法的解析:和接口方法不同,类方法可以类直接调用,接口方法必须要有实现的对象才可调用
- 若在类方法中发现index_class中索引的Simple是一个接口而不是一个类,则直接返回错误。
- 在Simple类中查找是否有方法描述和目标方法一致的方法,如果有直接返回引用,否则直接向上查找
- 如果父类中仍然没有找到,则意味着查找失败,程序会抛出NoSuchMethodError异常
- 如果在当前类或者父类中,找到了和目标方法一致的方法,但是它是一个抽象类,则会抛出AbstractMethodError异常
- 接口方法的解析:接口不仅可以定义方法,还可以继承其他接口
- 在接口方法表中发现class_index中索引的Simple是一个类而不是一个接口,则会直接返回错误,因为方法接口表和类接口表所容纳的类型应该是不一样的,这也是为什么在常量池中必须要有Constant_Methodref_info和Constant_InterfaceMethodred_info两个不同的类型
- 接下来的查找和类方法比较相似了,自下而上的查找,直到找到为止,或者没有找到抛出NoSuchMethodError异常
4、类的初始化阶段
4.1 <cinit>()
方法
- clinit = class initialize
<clinit>()
方法中包含了所有静态变量的赋值动作和静态语句块的执行代码,能保证顺序性
静态语句块只能对后面的静态变量进行赋值但是不能对其进行访问
public class ClassInit {
static {
System.out.println(x);//编译报错
x = 1000;
}
public static int x = 10;
}
- 虚拟机会保证父类的()方法最优先执行,因此父类的静态变量总是能够得到优先赋值
public class ClassInit {
static {
//System.out.println(x);//编译报错
x = 1000;
}
public static int x = 10;
static class Parent{
static int value = 10;
static {
value = 20;
}
}
static class Child extends Parent{
static int i = value;
}
public static void main(String[] args) {
System.out.println(Child.i);//会输出20 , 因为虚拟机会保证父类的init方法最优先执行
}
}
- Java编译器会帮助class生成cinit方法,但是cinit方法并不是总是会生成,某个类中既没有静态代码块也没有静态变量,就不会生成,接口也是如此。
- cinit只能被虚拟机执行,在触发了某个类的初始化就会执行
- 多线程初始化- 只能有一个线程执行到静态代码块中的内容,且静态代码块仅会执行一次、JVM保证
<cinit>()
方法在多线程环境下的同步语义,因此单例设计模式下,采用Holder的方式是一种最佳的设计方案
public class ClassInit {
static {
//System.out.println(x);//编译报错
//x = 1000;
try {
System.out.println(" The ClassInit static code block will be invoke ");
TimeUnit.SECONDS.sleep(10);
System.out.println("10秒之后输出");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static int x = 10;
static class Parent{
static int value = 10;
static {
value = 20;
}
}
static class Child extends Parent{
static int i = value;
}
public static void main(String[] args) {
//System.out.println(Child.i);//会输出20 , 因为虚拟机会保证父类的init方法最优先执行
IntStream.range(0,5).forEach(i->new Thread(ClassInit::new));// The ClassInit static code block will be invoke 10秒之后输出
}
}
The ClassInit static code block will be invoke
10秒之后输出
如果设置时间长,该类会一直等待直到输出
5、输出
public class Singleton {
//1
public static int x = 0;
public static int y;
private static Singleton instance = new Singleton();//2
private Singleton(){
x ++;
y ++;
}
public static Singleton getInstance(){
return instance;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(Singleton.x); // 1 0
System.out.println(Singleton.y); // 1 1
//1 2 位置 输出 1,1
cinit初始化阶段 x,y初始值 = 0 new Singleton()对x y 累加1
//2放在1位置 输出 0 ,1
cinit new Singleton()对x y 累加1 x,y初始值 = 1,1 ,x显示赋值=0 y没有所以是初始化值1
}
}