类的加载过程
ClassLoader主要职责就是加载各种class文件到JVM中,他是一个抽象的类,给定一个class的二进制文件名,他会尝试加载并且在Jvm中生成构成这个类的各个数据结构,然后使其分布在Jvm对应的内存区域中。
类的加载过程分为三个阶段,加载阶段,连接阶段,和初始化阶段
加载阶段
主要负责查找并且加载类的二进制文件,其实就是class文件,将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆中生成一个该类的class对象作为访问方法区数据结构的入口
栈内存:ClassLoader的引用,Aclass对象的引用,A对象的引用
堆内存:ClassLoader对象,A的class对象,A对象
方法区:A类的数据结构
不管某个类被加载了多少次,对应到堆内存中的class对象始终是同一个,他是类加载的最终产物。
连接阶段分为三个部分:验证、准备、解析
验证:
1.验证文件格式
- 二进制文件头部的魔术因子,该因子决定了文件是什么类型
- 主次版本号,高版本的Jvm编译的class不能被低版本的Jvm所兼容
- class文件的字节流是否残缺(每个类在编译的时候会经过md5摘要算法之后会作为字节流的一部分)
- 常量池中的常量是否存在不被支持的变量类型
- 指向常量的引用是否指到了不存在的常量或者该常量的类型不被支持
- 元数据的验证(对class的字节流进行语义分析,确保符合Jvm规范)
- 检查这个类是否存在父类,是否继承了某个接口,这些父类和接口是否合法或者存在
-检查该类是否继承了被final修饰的类,被final修饰的类是不允许被继承的,并且其中的方法是不允许被覆盖的 - 检查该类是否是抽象类,如果不是,是否实现了父类的抽象接口中的所有方法
- 检查方法重载的合法性,比如相同的方法名称,参数但是返回类型不相同是不被允许的
- 字节码的验证
- 保证当前线程在程序计数器中的指令不会跳转到不合法的字节码指令
- 保证类型的转换是否合法,比如用A声明的引用,不能用B进行强制转换
-保证任意时刻,虚拟机栈中的操作栈类型与指令代码都能正确的被执行,比如压栈的时候传入的是A类型的引用,在使用的时候却将B类型载入了本地变量表
- 符号引用验证(主要是验证某个类的字段不存在,或者方法不存在抛出异常,反射的时候比较多见)
- 通过符号引用描述的字符串全限定名称是否能够顺利的找到相关的类
- 符号引用中的类,方法,字段,是否对当前类可见,比如不能访问引用类的私有方法
5.其他验证
准备:
当一个class的字节流通过了所有的准备验证过程之后,就开始为该对象的类变量(静态变量)分配内存且设置初始值,类变量的内存会被分配到方法区中,不同于实例变量会被分配到堆内存中,所谓分配初始值,就是为相应的类变量给定一个相关类型在没有被设置值时的默认值
public static int a=10,在连接阶段初始值是0,
public final static int b=10,不会导致类的初始化,是一种被动引用,他不存在连接阶段,他在编译阶段就会被直接赋值为10
解析:
Simple simple=new Simple();
System.out.println(simple.name());
如上 我们可以通过simple访问Simple中的可见属性和方法,但是在字节码中,他会被编译成相应的助记符(符号引用),在类的解析过程中助记符还需要进一步解析才能正确的找到所对应的堆内存中的Simple数据结构。
- 字段解析
在解析类或者变量的时候比如上述Simple在调用他的属性和方法的时候,如果该字段不存在,或者出现错误就会抛出异常
如果Simple中本身就包含这个字段,则直接返回这个字段的引用,当然也要对该字段所属的类提前进行类加载
如果Simple中没有该字段,则会根据继承关系自下而上,查找父类或者接口的字段,找到即可返回,同样也需要提前对找到的字段进行类的加载过程
如果Simple中没有字段,一直找到了Object还是没有,则表示查找失败,直接抛出异常
这就解释了子类为什么重写了父类的字段之后能够生效的原因,因为找到子类的字段就直接初始化返回了 - 类方法的解析
类方法和接口不同,类方法可以直接使用该类调用,接口方法必须要有相应的实现类继承才能够进行调用
若在类方法表中发现索引的Simple是一个接口不是一个类,则直接返回错误
在Simple中查找是否有目标方法,如果有直接返回该方法的引用,否则继续向上查找
如果父类中仍然没有找到则抛出异常
如果在当前类或者父类中找到了目标方法,但是它是一个抽象类,则也会抛出异常 - 接口方法的解析
接口不仅可以定义方法,还可以继承其他接口
如果在接口方法表中发现索引的simple是一个类而不是接口,则会直接返回错误,因为方法接口表和类接口表所容纳的类型应该是不一样的
接下来的查找就和类方法的解析比较类似了,自下而上查找,直到找到为止,如果没有抛出异常 - 类接口的解析
如果simple是一个数组类型,则虚拟机不需要完成对simple的加载,只需要在虚拟机中生成一个能够代表该类型的对象,并且在堆内存中开辟一片连续的地址空间即可
初始化阶段主要为类的静态变量赋予正确的初始值(代码编写阶段给定的值)
每个类或者接口被java程序首次主动使用时,JVM才会对其进行初始化,所以程序包中那么多类并不是一次性全部被JVM初始化的,他很lazy.
静态语句块只能对后面的静态变量赋值,不能访问,否则无法通过编译
static{
System.out.println(x);
x=100;
}
private static int x=10;
虚拟机会保证父类的<clinit>方法最先执行,因此父类的静态变量总是能够得到优先赋值
主动使用的场景如下:
1.通过new关键字(会导致类的加载和最终初始化)TestApplication test=new TestApplication();
2.访问类的静态变量,读取和更新都会导致类的初始化 public static int x=10;但是,要记住,引用类的静态常量不会导致类被初始化,如 public final static int x=10;他是一个常量,不过需要计算的常量如:public final static int RANDOM=new Random().nextInt()例外,由于他在类的加载和和连接阶段无法对其进行计算,需要进行初始化后才能对其赋值。
3.访问类的静态方法也会导致类的初始化
4.对某个类进行反射操作会导致类的初始化 Class.forName("packageName+ClassName");
5.初始化子类会导致父类也被初始化,比如访问子类的静态变量或者静态方法,或者new一个子类,都会导致父类也被初始化,但是通过子类使用父类的静态变量或者静态方法只会导致父类被初始化,子类不会被初始化