在深入理解JVM–JVM类加载机制(上)中,只写到Java类加载机制的加载阶段,在类的生命周期中,还有后面的验证、准备、解析、初始化、使用和卸载这些阶段。
验证阶段的主要工作是为了确保Class文件流中包含的内容符合虚拟机的要求,而且不会威胁到虚拟机自身的安全,主要有以下几个方面:
准备阶段主要为类变量 在方法区中分配内存并设置其初始值,此时的初始值为各种变量的默认零值。
解析阶段主要是将Class文件中常量池内的符号引用替换为直接引用。
类的初始化是类加载过程的最后一步,在且仅在以下情况下,将触发类的初始化,它们是:
以上5种情况统称为对类的主动引用,对类的主动引用将触发类的初始化。其余情况对类的引用称为对类的被动引用。
以下被动引用不会触发类的初始化:
这一步的主要工作为生成并执行
初始化子类前会初始化父类。虚拟机会优先执行父类的
初始化阶段完成后,就是类的使用了,如果使用创建了类的实例对象,就涉及到类的实例化了。
方式 | 是否调用构造函数 |
---|---|
new关键字 | 是 |
Constructor类的newInstance方法 | 是 |
clone方法 | 否 |
反序列化 | 否 |
PS:Class类的newInstance方法是Constructor类的newInstance方法支撑的。
在调用构造函数来实例化对象的过程中,满足类初始化的触发条件,将会触发类的初始化,再进行实例化。
注:静态变量和静态代码块的初始化在类的初始化过程已经执行了,这里将不再赘述。
对象的实例化操作在字节码层面分为两步:1、使用new关键字在堆上分配内存,创建对象。2、执行
我们定义一个Father类:
public class Father {
public Father(){
System.out.println("no param");
}
public Father(String s){
System.out.println("param s");
}
public static void main(String[] args) {
Father f = new Father();
}
}
其对应字节码如下:
// class version 51.0 (51)
// access flags 0x21
public class cn/john/test/Father {
// compiled from: Father.java
// access flags 0x1
//对应无参构造函数Father()
public <init>()V
L0
LINENUMBER 31 L0
ALOAD 0
//Father继承自Object,优先调用Object的方法
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 32 L1
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
//再执行Father()中的剩余初始化逻辑
LDC "no param"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L2
LINENUMBER 33 L2
RETURN
L3
LOCALVARIABLE this Lcn/john/test/Father; L0 L3 0
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x1
//对应有参构造函数Father(String s)
public <init>(Ljava/lang/String;)V
L0
LINENUMBER 35 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 36 L1
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "param s"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L2
LINENUMBER 37 L2
RETURN
L3
LOCALVARIABLE this Lcn/john/test/Father; L0 L3 0
LOCALVARIABLE s Ljava/lang/String; L0 L3 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 40 L0
//新建对象,首先通过NEW关键字新建Father对象
NEW cn/john/test/Father
DUP
//调用Father类的构造函数进行初始化工作
INVOKESPECIAL cn/john/test/Father.<init> ()V
ASTORE 1
L1
LINENUMBER 41 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
LOCALVARIABLE f Lcn/john/test/Father; L1 L2 1
MAXSTACK = 2
MAXLOCALS = 2
}
由此我们可知,调用Father的构造函数新建Father类的实例,会先通过NEW关键字分配内存新建一个对象,然后优先调用Object的
再来看Son类:
public class Son extends Father{
private String name = new String("son");
{
System.out.println("construct block");
}
public Son(){
System.out.println("son no param");
}
public Son(String s){
System.out.println("son param s");
}
public static void main(String[] args) {
Son s = new Son();
}
}
其对应的字节码:
// class version 51.0 (51)
// access flags 0x21
public class cn/john/test/Son extends cn/john/test/Father {
// compiled from: Son.java
// access flags 0x2
private Ljava/lang/String; name
// access flags 0x1
//对应无参构造器
public <init>()V
L0
LINENUMBER 38 L0
ALOAD 0
//优先调用父类初始化方法
INVOKESPECIAL cn/john/test/Father.<init> ()V
L1
LINENUMBER 32 L1
ALOAD 0
NEW java/lang/String
DUP
//初始化实例变量
LDC "son"
INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
PUTFIELD cn/john/test/Son.name : Ljava/lang/String;
L2
LINENUMBER 35 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
//调用构造代码块
LDC "construct block"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
LINENUMBER 39 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
//执行构造函数逻辑
LDC "son no param"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 40 L4
RETURN
L5
LOCALVARIABLE this Lcn/john/test/Son; L0 L5 0
MAXSTACK = 4
MAXLOCALS = 1
// access flags 0x1
//对应有参构造函数
public <init>(Ljava/lang/String;)V
L0
LINENUMBER 42 L0
ALOAD 0
//为什么这里调用的是父类无参构造方法,下文再作说明
INVOKESPECIAL cn/john/test/Father.<init> ()V
L1
LINENUMBER 32 L1
ALOAD 0
NEW java/lang/String
DUP
LDC "son"
INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
PUTFIELD cn/john/test/Son.name : Ljava/lang/String;
L2
LINENUMBER 35 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "construct block"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
LINENUMBER 43 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "son param s"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 44 L4
RETURN
L5
LOCALVARIABLE this Lcn/john/test/Son; L0 L5 0
LOCALVARIABLE s Ljava/lang/String; L0 L5 1
MAXSTACK = 4
MAXLOCALS = 2
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 48 L0
NEW cn/john/test/Son
DUP
INVOKESPECIAL cn/john/test/Son.<init> ()V
ASTORE 1
L1
LINENUMBER 49 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
LOCALVARIABLE s Lcn/john/test/Son; L1 L2 1
MAXSTACK = 2
MAXLOCALS = 2
}
看过这两个例子,可见实例化的过程实际上分两步:
这里执行对象的初始化方法顺序为:先执行父类的构造方法,再执行本类的构造代码块,再执行本类的构造方法。
至此,在通过构造函数实例化对象过程中,实际上按照顺序发生了如下操作:
其中1~2为类加载过程中的类初始化,3~8为实例化过程中的初始化。
构造代码块存在的意义是抽取了类中多个构造函数的公共部分或需要提前执行初始化的部分优先执行,更加优雅,实际上跟将构造代码块分散到各个构造函数的最前部分是一致的,从字节码的结果可以看到编译器实际上也是这么做的。
this关键字为在类的构造函数之间互相调用提供了条件,同样是更加优雅的书写构造函数的方式。注意在构造代码块和this同时存在的情况下构造代码块只会执行一次,同时this调用必须位于使用它的构造函数的第一行。
super关键字可以调用父类构造器。上文分析字节码时可以看到子类在初始化过程中总是优先调用父类的初始化方法,这里可以看成子类的构造函数在第一行加入了super(),调用了父类的无参构造器。当然,也可以指定调用有参构造器super(Type …args),只是在没有写明的情况下,编译器默认调用的是父类的无参构造器。
最后,类的生命周期中卸载问题,就是GC的事了。
深入理解Java虚拟机