我们写的代码会首先编译成.class
文件,然后加载到虚拟机中后才能运行和使用,那么虚拟机是如何加载这些class文件的呢?会有哪些过程呢?今天我们来探究一下虚拟机的类加载机制。
类被加载到虚拟机中,它的整个生命周期分为:加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备和解析部分统称为连接。
我们主要关注加载、连接和初始化部分。
类的加载指的是将类的.class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class
对象(jdk1.7放在方法区,1.8放在metaspace)用来封装类在方法区内的数据结构
加载.class文件的方式
这一阶段的目的是为了确保Class文件的字节流中包含的信息是符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段分为4个动作:
文件格式验证
验证字节流是否符合Class文件格式的规范,保证输入的字节流能够被正确的解析和存储于方法区之内。
元数据验证
对字节码描述的信息进行语义分析,如检查这个类的父类是否继承了不允许被继承的类(被Final修饰的类),主要是对类的元数据信息进行语义校验。
字节码验证
通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,主要是对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出伤害虚拟机安全的事件。
符号引用验证
确保解析动作能正常执行,主要对类自身以外的信息(常量池中的各种符号引用)进行校验
验证阶段不是必要的,如果代码已经被反复使用和验证过,可考虑使用-Xverify:none
参数关闭验证,缩短类加载的时间
这一阶段主要是为「类的静态变量」分配内存和设置默认值,如int类型默认值为0,引用类型默认值为null等
这一阶段主要是将常量池中的符号引用替换为直接引用,如将字符串全类名(com.xxx.xxx)替换为内存地址
这一阶段就是为类的静态变量显式地赋予其初始值,即程序员设定的值
Java程序对类的使用方式可分为两种:
虚拟机规范中要求每个类或接口被Java程序「首次主动使用」时需要进行初始化
「主动使用」
「被动使用」
下面举几个例子来理解一下:
主动使用例子:
public class Test {
public static void main(String[] args) {
System.out.println(Child.str2);
}
}
class Parent {
public static String str = "hello world";
static {
System.out.println("parent static block");
}
}
class Child extends Parent {
public static String str2 = "hello beauty";
static {
System.out.println("child static block");
}
}
//parent static block
//child static block
//hello beauty
主动使用了子类,在初始化子类之前会先初始化父类,再初始化子类
被动使用例子1:
// 对于静态字段来说,只有直接定义了该字段的类才会被初始化
public class Test {
public static void main(String[] args) {
System.out.println(Child.str);
}
}
class Parent {
public static String str = "hello world";
static {
System.out.println("parent static block");
}
}
class Child extends Parent {
static {
System.out.println("child static block");
}
}
//parent static block
//hello world
通过子类引用父类的静态变量,并不会初始化子类,因为没有直接使用到了子类的静态变量。
被动使用例子2:
public class TestMain {
public static void main(String[] args) {
Parent[] parents = new Parent[1];
}
}
class Parent {
static {
System.out.println("parent static block");
}
}
//无输出
通过数组定义来引用类,并不会触发此类的初始化,对于数组来说,Java虚拟机会自动生成一个类来进行初始化,比如Parent数组会生成一个名为[Lcom.dxg.demo.Parent的类。
被动使用例子3:
//代码1
public class TestMain {
public static void main(String[] args) {
System.out.println(Parent.i);
}
}
class Parent {
public static final int i = 1;
static {
System.out.println("parent static block");
}
}
//1
这个例子只输出了hello world,并不会输出静态块的内容。因为常量在编译阶段会存入调用这个常量所在方法的类的常量池中,本质上并没有直接引用到定义常量的类,所以不会触发该类的初始化。
但是要注意运行时常量并不是如此,如下代码:
//代码2
public class TestMain {
public static void main(String[] args) {
System.out.println(Child.j);
}
}
class Parent {
public static final int i = 1;
static {
System.out.println("parent static block");
}
}
class Child extends Parent {
public static final int j = new Random().nextInt();
static {
System.out.println("child static block");
}
}
//parent static block
//child static block
//1394803640
运行时常量在编译阶段是不知道其值的,只有在运行时才能获取到值,所以使用运行时常量会触发类的初始化
我们也可以看看javap -c
反编译后的结果:
//代码1
public class com.guodx.studydemo.jvm.TestMain {
public com.guodx.studydemo.jvm.TestMain();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1
4: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
7: return
}
//代码2
public class com.guodx.studydemo.jvm.TestMain {
public com.guodx.studydemo.jvm.TestMain();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #3 // Field com/guodx/studydemo/jvm/Child.j:I
6: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
9: return
}
注意代码1main
方法的行号3调用的是iconst_1
,说明编译时就把常量放到常量池中了,再看代码2main
方法行号3则是getstatic
,说明主动使用了类,调用了类的静态字段,会初始化类。
「特殊情况」
当一个类或接口在初始化时,并不要求其父接口都完成了初始化,只有当真正使用到父接口的时候(如引用父接口中定义的常量时,才会初始化)
public class TestMain {
public static void main(String[] args) {
System.out.println(Child.i);
}
}
interface Parent {
public static Thread thread = new Thread() {
{
System.out.println("parent invoked");
}
};
}
class Child implements Parent {
public static int i = 5;
static {
System.out.println("child static block");
}
}
//child static block
//5
public class TestMain {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1=" + Singleton.counter1);
System.out.println("counter2=" + Singleton.counter2);
}
}
class Singleton {
public static int counter1;
private static final Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}
public static int counter2 = 0;
public static Singleton getInstance() {
return singleton;
}
}
先来一段代码,思考一下其运行结果?理解了这段代码也就理解了类的初始化顺序了
....
揭晓答案
counter1=1
counter2=0
首先我们来回顾一下类的生命周期,加载、验证、准备、解析、初始化、使用和卸载七个阶段,在准备阶段会给类的静态变量赋予初始值,此时counter1=0
,singleton=null
,counter2=0
,然后进入初始化阶段,该阶段给类的静态变量赋予正确的值,此时counter1=0
,singleton=new Singleton()
,进入构造方法 counter1++;
counter2++;
,现在counter1=1
,counter2=1
了,但是最后轮到counter2
显式设置为0则此时counter2=0
。
前面我们知道了子类在初始化的时候会先初始化父类,再结合上面的类初始化顺序,我们可以得出类初始化时会先初始化其父类,再初始化子类,并且类中的静态变量是按顺序依次进行初始化的。
这个小节我们学习了三大块内容:
原文:https://www.cnblogs.com/dxGo/p/14698518.html