Java 是一门面向对象的语言,所以除了基本类型以外都是对象,那么这些对象是怎样从 class 文件加载到虚拟机,然后成为内存中一个个对象的呢?本文就来分析下 Java 中的类是如何初始化的?包括类初始化,实例初始化,构造器等。
一、类加载时机
类的生命周期:从被加载到虚拟机内存中开始,到卸载出内存。一共包括如下阶段:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading),其中验证,准备,解析 3 个阶段统称为连接(Linking)。
那么,在什么样的时机下会触发类的生命周期呢?虚拟机规范严格规定了有且只有以下 5 中情况必须立即对类进行初始化(加载,验证,准备自然在初始化之前进行):
- 遇到
new
、getstatic
、putstatic
或invokestatic
这 4 条字节码指令时,如果类未进行过初始化,那么需首先触发类的初始化。分别对应的场景为:1、使用new
关键字实例化对象时;2、读取类的静态变量时(被final
修饰,已在编译期把结果放入常量池的静态字段除外);3、设置类的静态变量时;4、调用一个类的静态方法时。 - 使用反射对类进行调用时。
- 初始化一个类时,如果其父类还未初始化,那么会先触发其父类的初始化。
- 当虚拟机启动时,被指定为需要执行的那个主类(
main()
方法所在的类),虚拟机需要先初始化。 - JDK 1.7 动态语言支持,详见《深入理解 Java 虚拟机》。
其中 1、new
关键字实例化对象是调用对象的构造方法,构造方法也是静态方法的一种(《Java 编程思想》);4、main
方法也是静态方法,所以触发类初始化的时机就是:调用类的静态对象。
现在,类初始化的时机我们清楚了,那么类是如何初始化的呢?实例又是如何初始化的?涉及继承时又是如何初始化的?
二、类初始化,实例初始化
要搞懂类初始化的过程,就要搞懂类初始化和实例初始化。
- 类初始化
<cinit>
:就是类第一次加载到内存时进行的过程,也就是我们在上一节讨论的内容。类初始化只进行一次(前提是被同一类加载器加载),后续使用new
等实例化对象时都不在进行初始化了,所以类初始化只运行一次。初始化的都是属于类(而不是实例)的内容(静态),所以对所有实例共享。 - 实例初始化
<init>
:也就是实例化对象时每次都会进行的过程,初始化属于实例的内容(非静态),没有实例所拥有的实例内容是不共享的,独有的。
2.1 类初始化
前面已经讨论了类进行初始化的时机,下面就来说说类初始化会干些什么。
在类初始化之前的准备阶段,虚拟机会将类变量(static
修饰的变量)分配内存并设置零值。
在类初始化阶段,执行类构造器 <cinit>()
方法。<cinit>
类初始化方法有如下特点:
- 编译器会在将
.java
文件编译成.class
文件时,收集所有类初始化代码和static {}
域的代码,收集在一起成为<cinit>()
方法; - 子类初始化时会首先调用父类的
<cinit>()
方法; - JVM 会保证
<cinit>()
方法的线程安全,保证同一时间只有一个线程执行;
2.2 实例初始化
构造器:保证实例正确的初始化,能被使用。
所以,既然子类继承了父类,那么子类调用构造函数初始化的,就需要调用父类的构造器,不管是显示调用父类构造器还是 JVM 自动调用,这样才能保证子类正确的被构造。
那么,实例初始化的过程到底是如何的呢?
- JVM 收集实例初始化变量和
{}
域组合成实例初始化方法<init>()
; - 实例初始化时首先执行
<init>()
方法,然后执行构造函数; - 子类通过构造函数构造实例时会首先调用父类的
<init>()
方法和父类的构造函数,如果没有显示调用父类的构造函数,那么 JVM 会自动调用父类的无参构造函数,保证父类构造函数一定被调用,然后再是子类自己的<init>()
方法和构造函数; - 至此,实例就构造完毕了;
现在,大概应该清楚类和实例如何初始化和被构造了:
- 父类类初始化
<cinit>()
; - 子类类初始化
<cinit>()
; - 父类
<init>()
+ 父类构造器; - 子类
<init>()
+ 子类构造器;
三、示例:组合的初始化
package acherie.resuing;
class Window {
static {
System.out.println("Window static");
}
Window (int marker) {
System.out.println("Window(" + marker + ")");
}
}
class House {
static {
System.out.println("House static");
}
Window w1 = new Window(1);// Before constructor
House () {
System.out.println("House()");
w3 = new Window(33);
}
Window w2 = new Window(2);
void f() {
System.out.println("f()");
}
Window w3 = new Window(3);
}
public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f();
}
}
运行结果如下:
House static
Window static
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
Process finished with exit code 0
其实,使用反编译软件(如:jd-gui,或 intellij idea 自带的)看一看编译后生成的代码,很多问题就清楚了:
// House.class
package acherie.resuing;
import acherie.resuing.Window;
class House {
Window w1 = new Window(1);
Window w2 = new Window(2);
Window w3 = new Window(3);
House() {
System.out.println("House()");
this.w3 = new Window(33);
}
void f() {
System.out.println("f()");
}
static {
System.out.println("House static");
}
}
// OrderOfInitialization.class
package acherie.resuing;
import acherie.resuing.House;
public class OrderOfInitialization {
public OrderOfInitialization() {
}
public static void main(String[] args) {
House h = new House();
h.f();
}
}
四、示例:继承的初始化
package acherie.resuing;
class Person {
private int i = 8;
protected int j;
static {
System.out.println("Person 静态初始化子句");
}
{
System.out.println("Person 实例初始化子句");
}
Person () {
System.out.println("Person()");
}
Person(int i) {
System.out.println("Person(int), i=" + i);
}
int k = printInit("Person.k 初始化");
static int m = printInit("static Person.m 初始化");
{
System.out.println("Person 后置实例初始化语句");
}
static int printInit(String s) {
System.out.println(s);
return 47;
}
}
public class Student extends Person {
Student () {
super(1);
System.out.println("Student()");
}
Student(int i) {
this();
System.out.println("Student(int), i=" + i);
}
public static void main(String[] args) {
new Person(3);
System.out.println("--------------------------");
Student student = new Student(2);
}
public static int marker = printInit("Student.marker 初始化");
{
System.out.println("Student 实例初始化域");
}
}
运行结果如下:
Person 静态初始化子句
static Person.m 初始化
Student.marker 初始化
Person 实例初始化子句
Person.k 初始化
Person 后置实例初始化语句
Person(int), i=3
--------------------------
Person 实例初始化子句
Person.k 初始化
Person 后置实例初始化语句
Person(int), i=1
Student 实例初始化域
Student()
Student(int), i=2
Process finished with exit code 0
使用反编译软件查看编译后的 class 文件:
// Person.class
package acherie.resuing;
class Person {
private int i = 8;
protected int j;
int k;
static int m;
Person() {
System.out.println("Person 实例初始化子句");
this.k = printInit("Person.k 初始化");
System.out.println("Person 后置实例初始化语句");
System.out.println("Person()");
}
Person(int i) {
System.out.println("Person 实例初始化子句");
this.k = printInit("Person.k 初始化");
System.out.println("Person 后置实例初始化语句");
System.out.println("Person(int), i=" + i);
}
static int printInit(String s) {
System.out.println(s);
return 47;
}
static {
System.out.println("Person 静态初始化子句");
m = printInit("static Person.m 初始化");
}
}
// Student.class
package acherie.resuing;
import acherie.resuing.Person;
public class Student extends Person {
public static int marker = printInit("Student.marker 初始化");
Student() {
super(1);
System.out.println("Student 实例初始化域");
System.out.println("Student()");
}
Student(int i) {
this();
System.out.println("Student(int), i=" + i);
}
public static void main(String[] args) {
new Person(3);
System.out.println("--------------------------");
new Student(2);
}
}
六、总结
本文主要介绍了类和示例如何初始化的,并辅以示例。其实,在研究这个问题时最大的感受在于,通过查看编译后生成的 class
文件,其实很多不懂的东西都能够解决和更好的理解。