1. 类加载的时机
1. 类加载的起个过程及相对执行顺序
上图加载 验证 准备 初始化 卸载 这5个阶段是俺按部就班开始的,不是执行完一个再执行一个,而是这些阶段互相交叉地混合进行.解析可以在初始化完成之后.
2. 触发初始化的6个操作
- 遇到new getstatic putstatic 或 invokestatic这四条指令时
- new关键字创建对象
- 读取或设置一个静态字段时(final修饰除外,编译器已经放入常量池中)
3.调用一个类的静态方法时
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行初始化,则先进行初始化.
- 初始化类的时候,发现其父类还没有初始化,首先初始化其父类(== 回答了为什么JVM保证其父类优先初始化==)
- 虚拟机启动时,虚拟机会首先初始化包含main()方法的那个类
- 如果接口定义了默认方法,如果这个接口的实现类发生了初始化,那么该接口需要在其之前初始化.
- 略
3.被动引用举例
上述六中场景会触发类的初始化,这六种场景中的行为称为主动引用,其余方式的称为被动引用.
子类引用父类静态字段
- 代码
public class SupperClass {
static{
System.out.println("SuperClass init");
}
public static int value=123;
}
public class SubClass extends SupperClass {
static{
System.out.println("SubClass init");
}
public class NotInitialization {
/**
* 子引用父类静态字段,不会初始化子类
*/
@Test
public void test1(){
System.out.println(SubClass.value);
}
}
- 程序输出
SuperClass init
123
- 解释
对于静态字段的引用,只有直接定义这个字段的类才会被初始化,通过子类来引用父类的静态字段,只会触发父类初始化. - 拓展
如果父类的静态字段有final修饰呢
public static final int value=123;
System.out.println(SubClass.value);
输出为:
SuperClass init
123
但理论上被final修饰后,已经在编译器将结果放入常量池中了, why还是会初始化?
System.out.println(SupperClass.value);
输出为:123
解释:编译阶段已经将123存入常量池中了,该类对常量的引用被转化为对自身常量池的引用.
5. 继承关系图
2.类加载的过程
1. 加载
加载过程将虚拟机外部的字节码文件(二进制字节流文件)存储在方法区之中,具体过程如下
- 通过类的==全限定类名(相当于位置坐标)==获取定义此类的二进制字节流
该过程<<java虚拟机>>虚拟机规范并没有要求从哪获取.class文件,所以才出现了 jar(压缩文件获取) jsp(从文件中获取) 等技术. - 将这个字节流所代的静态存储结构转换成方法区的运行时数据结构
即将外部二进制字节流(.class) 文件按照虚拟机所设定的格式存储在方法区中. - 在内存中(堆内存)生成代表这个类的java.lang.class对象,作为对方法区的这个类各种数据的访问入口.
2. 验证
验证的目的是class文键字节流包含的信息符合 <<虚拟机规范>>的全部约束要求,保证这些代码运行后不会危害虚拟机自身安全.主要包括 文件格式验证,元数据验证,字节码验证,福海引用验证.
3. 准备(重要 P272)
准备阶段正式为类中定义的变量(static)分配内存并赋初始值
这些变量所拥有的内存在JDK7以前存在方法区(永久代实现),但JDK7以后,类变量会随着class对象存放在java堆中分配完内存会赋初始值(零值).实例变量将会在对象实例化时随对象一起分配在java堆中.当类字段的字段属性表中存在ConstantValue属性时,==在准备阶段直接将 final static 的值设置为该属性的属性值(直接初始化)==其它情况的类变量的初始化发生在类的初始化阶段
4. 解析 P272-P277
- 解析将常量池 内的符号引用转换为直接引用.我的理解是符号引用是名,直接引用是实.在进行符号引用时,相当于只引用了一个名字,例如 小明,小张.把常量池中的符号引用变为直接引用相当于把名字直接换成了这个人(虽然JVM中是地址).直接引用是指可以直接指向目标的指针,相对偏移量,或句柄.
- 解析针对的对象
- 举例
package chpter7.T7_4;
/**
* @ClassName FieldResolution
* @Description TODO
* @Author 86134
* @Date 2022/8/31 17:54
* @Version 1.0
**/
public class FieldResolution {
interface interface0{
int A=0;
}
interface interface1 extends interface0{
int A=1;
}
interface interface2{
int A=2;
}
static class Parent implements interface1{
public static int A=3;
}
static class Sub extends Parent implements interface2{
// public static int A=4;
}
public static void main(String[] args) {
System.out.println(Sub.A);
}
}
输出:
Error:(30, 31) java: 对A的引用不明确
chpter7.T7_4.FieldResolution.Parent 中的变量 A 和 chpter7.T7_4.FieldResolution.interface2 中的变量 A 都匹配
5. 初始化(重要)
1. 概述
初始化阶段会初始化类变量和其它资源.本质上,初始化阶段就是执行类构造器<clinit>()方法的过程.
2. <clinit>()方法
<clinit>()由javac编译器自动生成. <clinit>()是由编译器收集类中的所有类变量赋值动作和静态语句块(static{}) 中的语句合并产生的,编译器收集的顺序是由源文件中语句出现的顺序决定的静态语句块只能==访问(不是赋值)==到定义到它之前的变量,如果访问它之后的变量,编译器会提示"非法的前向访问"(因为静态变量还没有初始化,不能访问).因为在准备阶段静态已经为静态变量分配空间并赋零值,所有可以在静态代码块中为它之后定义的变量赋值.例如
public class Test {
static {
i=1;
}
static int i ;
public static void main(String[] args) {
System.out.println(i);
}
}
输出:1
//static 修饰的变量其实不用初始化系统也不会报错,因为有赋
//零值的过程,但成员变量不行(錯誤)
public class Test02 {
public int i;
public static void main(String[] args) {
System.out.println(new Test02().i);
}
成員變量也可以
3. <clinit>()方法的一些性质
- <clinit>()与类的构造函数不同(构造函数在JVM层面执行init()方法,用来构造一个对象).它不需要显示的调用父类的 <clinit>()方法.==JVM保证子类的 <clinit>()方法执行之前,父类的 <clinit>()方法已经执行完毕(父类在解析阶段被加载进内存)==所以初始化阶段第一个执行的一定是Object的 <clinit>()方法.
- 父类的静态代码块先于子类执行
public class Parent {
public static int A=1;
static {
A=2;
}
}
public class Sub extends Parent {
public static int B=A;
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
// 输出 2
- 如果一个类中没有对静态变量的赋值操作(不是没有静态变量)与静态代码块,就不会生成 <clinit>()方法
- 当多线程环境中同时初始化一个类(不是同时创建一个类,什么时候会导致类的加载,看上文),必须保证其被加锁同步.如果一个类的 <clinit>()方法需要很长时间执行完,往往会造成多个线程的阻塞,不以察觉.