类加载过程
Class文件需要加载到虚拟机中才能运行,虚拟机加载Class类型文件主要的步骤如下:
- 加载、验证、准备、初始化这4个过程过程是按照这种顺序按部就班地开始的,而解析阶段不一定;
- 按顺序开始,而不是进行,因为这些阶段通常是相互交叉地混合进行的;
问题:什么时候必须立即对类进行“初始化”
加载、验证、准备自然需要在此之前开始;
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,代码场景:
能够生成这四条指令的典型Java代码场景:
- 使用new关键字实例化对象时;
- 读取/设置一个类型的静态字段时
- 调用一个类型的静态方法时
- 使用java.lang.reflect包的方法对类型进行反射调用的时候;
- 当初始化的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化;
- 当虚拟机启动的时候,用户需要执行一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类;
- 当一个接口被default关键字修饰了接口的方法,如果有这个接口的实现类发生了初始化,那么该接口要在之前被初始化;
这几种场景中的行为,被称为对一个类型进行主动引用; 出此之外,所有引用类型方式都不会被触发初始化,被称为被动引用;
被动引用案例:
- 通过子类引用父类的静态字段(对于静态字段,只有直接定义这个字段的类才会被初始化);
- 通过数组定义的引用类,不触发类的初始化;
- 常量在编译阶段会被存入调用类的常量中,本质上没有直接引用到定义常量的类,因此不触发定义常量类的初始化。
1、加载
“加载”阶段是整个“类加载”过程中的一个阶段(别混淆)
(1)加载步骤
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将此字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在堆内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
(2)特点
JVM规范中对加载的三个步骤点的要求不是特别具体,因此有很大的灵活度:
- 没有指明二进制字节流必须得从哪里获取、怎么获取,所以留给用户在类加载阶段很大的发挥空间(eg:从zip包中读取等)
- 非数组类型的加载阶段是开发人员可控性最强的阶段,可用JVM中内置的引导类加载器,也可以用用户自定义的类加载器去完成(重写一个类加载器的loadClass()方法)。
- 数组类不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的(但其中元素类型还是要靠类加载器来完成加载的)。
(3)加载阶段结束后的场景
- 被加载的类的二进制字节流按照虚拟机所设定的格式存储在方法区中;
- Java堆内存中会实例化出来一个java.lang.Class类的对象(程序访问方法区中类型数据的外部接口)
- 加载阶段和连接阶段内容交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
2、连接
连接阶段中三个小阶段的总结:
(1)验证
1、格式验证
验证字节流是否符合虚拟机的规范
- 是否以0xCAFEBABE开头;
- 主、次版本号是否在当前虚拟机的处理范围内
- 常量池中的常量是否有不被支持的类型
2、元数据验证 对字节码描述的信息进行语义分析(数据类型校验),以保证其描述的信息符合Java语言规范的要求
这个阶段可能包括的验证点如下:
- 这个类是否有父类(除了java.lang.Object外,所有类都有父类)
- 这个类的父亲是否继承了不被允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口中所要求实现的所有方法
- 类中的字段、方法是否与父类产生矛盾(覆盖了父类的final字段,出现不符合规范的方法重载…)
…
3、字节码验证 通过数据流和控制流分析,确保程序语义是合法的、符合逻辑的。比如保证任意时刻操作数栈和指令代码序列都能配合工作。
4、符号引用验证 发生时间: 此阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。 作用:确保解析行为能够正常执行 符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验。通俗来讲,该类是否缺少或被禁止访问它依赖的某些外部类、方法、字段等资源。
(2)准备
作用: 为类变量(静态变量,被static修饰的变量)分配内存并设置类变量初始值。
注意:
- 不包括实例变量,实例变量在对象实例化时随对象一块分配在Java堆中;
- 从概念上说,类变量使用的内存应该在方法区中分配(永久代时符合此逻辑)。但JDK8开始,HotSpot将原本放在永久代中的字符串常量池、静态变量等移动到了堆中,此时类变量会随着Class对象一起存放在Java堆中。
- 类变量的初始值“通常情况下”是数据类型默认的零值(eg:0、0L、null、false等)
“特殊情况”类字段被final修饰了public static final int value=111 ,那么准备阶段此类变量就会被赋值为111。
(3)解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。 通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
3、初始化
执行初始化方法<clinit>() 方法的过程
在准备阶段,类变量已经赋过一次零值了,而在初始化阶段,则会根据程序员编写的代码去初始化类变量。
<clinit> ()方法
1、如何产生? 该方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的。
2、非法前向引用变量 static块中只能访问到定义在static块之前的变量,定义在它之后的变量,在前面的static块中可以赋值,但是不能访问
3、如何对待父类?
- 与类的构造函数不同,
<clinit>() 方法不需要显式调用父类构造器(Java虚拟机保证在子类<clinit>() 执行前,父类的<clinit>() 已经执行完毕)。 - 父类中定义的静态语句块要优先于子类的变量赋值操作
3、非必需 <clinit>() 对类或接口来说并不是必须的,如果一个类中没有静态语句块,也就没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>() 方法。
4、接口的不同点
- 不能使用静态语句块,但仍有变量初始化的赋值操作
- 执行接口
<clinit>() 方法不需要先执行父类接口的<clinit>() - 只有当父类接口中定义的变量被使用时,父类接口才会被初始化
- 接口的实现类在初始化时也一样不会被执行接口的
<clinit>() 方法
5、线程同步 Java虚拟机必须保证一个类的<clinit>() 方法在多线程环境中被正确地加锁同步
如果多个线程同时去初始化一个类,那么只有其中一个线程去执行这个类的<clinit>() 方法,其他线程都阻塞等待,直到活动线程执行完毕<clinit>() 方法。
|