类的加载过程
类的加载指的是将类的.class文件中的二进制数据读入到内存中,然后在堆区创建一个这个类的java.lang.Class对象。
类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
一个java文件从被加载到被卸载这个生命过程,总共要经历7个阶段,JVM将类的生命周期过程分为:
加载
加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中。
加载类的方式有以下几种:
- 从本地系统直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件(服务器)
验证
验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如jdk版本是否兼容,高版本编译的class无法在低版本运行。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,父类是否存在。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
准备阶段是为类变量在方法区分配内存并设置默认值的阶段。对于该阶段有以下几点需要注意:
- 此时内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
- 此处设置的初始值是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
- 静态常量直接被赋予默认值。
先看下面一个例子:
package com.morris.jvm.load;
public class PrepareTest {
public static int a = 10;
public static final int b = 10;
}
在准备阶段a=0,而b=10。因为final修饰的常量(可直接计算出结果)不会导致类的初始化,是一种被动引用。更严谨的说法是常量在编译阶段会将其value生成一个ConstandValue,直接赋予10。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
初始化,为类的静态变量赋予正确的初始值,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
初始化阶段最重要的一件事就是执行<cinit>方法(class initialize,类构造器),此方法由jvm编译后生成,使用javap命令查看class字节码可看到此方法。
- 类构造器<cinit>:按源代码顺序收集类中所有静态代码块和类变量赋值语句组成。
- 对象构造器<cinit>:按源代码顺序收集成员变量赋值和普通代码块,最后收集对象的构造方法,最终组成对象构造器。
注意:静态代码块中只能对后面的变量进行赋值,不能进行访问。
static {
x = 20;
System.out.println(x); // 此行编译不通过
}
public static int x = 10;
再来看一个例子,深刻理解类的初始化:
package com.morris.jvm.load;
public class BookTest {
static BookTest book = new BookTest();
static int amount = 112;
static {
System.out.println("书的静态代码块");
}
int price = 110;
{
System.out.println("书的普通代码块");
}
BookTest() {
System.out.println("书的构造方法");
System.out.println("price=" + price + ",amount=" + amount);
}
public static void main(String[] args) {
staticFunction();
}
public static void staticFunction() {
System.out.println("书的静态方法");
}
}
运行结果如下:
书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法
对于BookTest类,其类构造器可以简单表示如下:
static BookTest book = new BookTest();
static {
System.out.println("书的静态代码块");
}
static int amount = 112;
对于BookTest类,其对象构造器可以简单表示如下:
{
System.out.println("书的普通代码块");
}
int price = 110;
BookTest() {
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);
}
}
类的初始化顺序如下:
- 如果这个类还没有被加载和连接,那先进行加载和连接
- 假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
- 假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
- 总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法
类的主动使用与被动使用
每个类只有在首次主动使用才会被初始化。
以下是类的6种主动使用场景。
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName)
- 初始化一个类的子类(会首先初始化子类的父类)
- 调用main方法
除了以上6种情况外,其余的都称为被动使用,不会导致类的初始化。
关于类的主动使用与被动使用,下面是几个容易混淆的例子:
构造某个类的数组时不会导致类的初始化
package com.morris.jvm.load;
public class ArrayLoadTest {
public static void main(String[] args) {
A[] arrays = new A[10]; // // 只不过是在堆内存开辟了4byte*10的空间
}
public static class A {
static {
System.out.println("class A init.");
}
}
}
引用类的静态常量不会导致类的初始化
package com.morris.jvm.load;
import java.util.Random;
public class ConstantTest {
public static void main(String[] args) {
System.out.println(A.MAX);
System.out.println(A.RANDOM);
}
public static class A {
public static final int MAX = 10; // 引用类的静态常量不会导致类的初始化
public static final int RANDOM = new Random().nextInt(); // 只有在初始化后才能得到具体值,会导致了类的初始化
static {
System.out.println("class A init.");
}
}
}
运行结果如下:
10
class A init.
-859057135
从下面的字节码可以看出常量A.MAX在编译阶段会直接赋予10,而常量A.RANDOM需要初始化ConstantTest类后才能得出结果。
// access flags 0x19
public final static I MAX = 10
// access flags 0x19
public final static I RANDOM
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 15 L0
NEW java/util/Random
DUP
INVOKESPECIAL java/util/Random.<init> ()V
INVOKEVIRTUAL java/util/Random.nextInt ()I
PUTSTATIC com/morris/jvm/classloader/ConstantTest$A.RANDOM : I
L1
LINENUMBER 18 L1
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "class A init."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L2
LINENUMBER 19 L2
RETURN
MAXSTACK = 2
MAXLOCALS = 0
}
使用
当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码。
卸载
JVM规定了一个Class只有在满足一下三个条件的时候才会被GC回收,也就是类的卸载。
- 该类所有的实例已经把GC。
- 该类的Class对象不再被引用,即不可触及。
- 加载该类的ClassLoader已经被GC。
面试题
最后以一个面试题来结束本文。
package com.morris.jvm.load;
public class StaticTest {
public static int k = 0;
public static StaticTest t1 = new StaticTest("t1");
public static StaticTest t2 = new StaticTest("t2");
public static int i = print("i");
public static int n = 99;
public int j = print("j");
{
print("构造块");
}
static{
print("静态块");
}
public StaticTest(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
++i;
}
public static int print(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++i;
return ++n;
}
public static void main(String[] args) {
new StaticTest("init");
}
}
运行结果如下:
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99
9:j i=8 n=100
10:构造块 i=9 n=101
11:init i=10 n=102
更多精彩内容关注本人公众号:架构师升级之路