类和实例的初始化

  1. 类的初始化

## 实例化过程

最近面试的时候遇到很多人都在问java初始化的东西, 今天就写个测试程序来个JAVA初始化大揭秘.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TestInit {
public static void main(String[] args) {
new B();
new B();
}
}

class A {

public A() {
System.out.println("A");
}

{
System.out.println("A init");
}

static {
System.out.println("A static init");
}
}

class B extends A {
public B() {
System.out.println("B");
}

{
System.out.println("B init");
}

static {
System.out.println("B static init");
}
}

这个程序的输出结果为

1
2
3
4
5
6
7
8
9
10
A static init
B static init
A init
A
B init
B
A init
A
B init
B

下面我用javap命令反编译一下TestInit的class字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  classes  javap -c TestInit
Compiled from "TestInit.java"
public class TestInit {
public TestInit();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2 // class B
3: dup
4: invokespecial #3 // Method B."<init>":()V
7: pop
8: new #2 // class B
11: dup
12: invokespecial #3 // Method B."<init>":()V
15: pop
16: return
}

然后看一下A的class字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜  classes  javap -c A
Compiled from "TestInit.java"
class A {
public A();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String A init
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #5 // String A
17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return

static {};
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String A static init
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

从上面代码的执行结果我们也可以看出, A的代码是先执行的static静态初始化的(这段代码只有在类被加载进虚拟机中时才会执行一次). 那么我们就先从它分析入手

  1. getstatic 访问java/lang/System.out这个实例熟悉
  2. ldc 从常量池里加载一个常亮进入操作数栈, 这里加载的是A static init字符串
  3. invokevirtual 然后调用java/io/PrintStream.println方法, 输出A static init字符串

构造器的代码开始执行

  1. aload_0 : 从局部变量表加载一个reference类型值到操作数栈, 这个变量应该是this
  2. invokespecial : 用于需要特殊处理的实例方法(实例初始化方法, 私有方法和父类方法). 这里是调用A的实例化方法, 也就是{}这中的代码
  3. getstatic 实例化方法访问java/lang/System.out属性
  4. ldc 实例化方法从常量池里加载一个常亮进入操作数栈, 这里加载的是A init字符串
  5. invokevirtual 实例化方法调用java/io/PrintStream.println方法, 输出A init字符串
  6. getstatic 构造器访问java/lang/System.out属性
  7. ldc构造器从常量池里加载一个常亮进入操作数栈, 这里加载的是A字符串
  8. invokevirtual 构造器调用java/io/PrintStream.println方法, 输出A字符串

然后我们看一下B的claa字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜  classes  javap -c B
Compiled from "TestInit.java"
class B extends A {
public B();
Code:
0: aload_0
1: invokespecial #1 // Method A."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String B init
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #5 // String B
17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return

static {};
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String B static init
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

与A类似, B同样是从类的初始化开始代码执行的

  1. getstatic 访问java/lang/System.out这个实例熟悉
  2. ldc 从常量池里加载一个常亮进入操作数栈, 这里加载的是B static init字符串
  3. invokevirtual 然后调用java/io/PrintStream.println方法, 输出B static init字符串

然后是构造器方法执行

  1. aload_0同样的是加载this进虚拟机栈
  2. invokespecial 调用父类A的实例初始化方法
  3. 然后就开死像A一样, 调用自己的实例化过程

如果我们只加载这个类呢?

1
2
3
4
5
public class TestInit {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("B");
}
}

结果输出为

1
2
A static init
B static init

这也间接地验证了我们上面的推算

类的初始化

类初始化阶段是类加载过程中最后一步,开始执行类构造器方法. <clinit>方法执行过程可能会影响程序运行行为的一些特点和细节

  1. <clinit>方法由编译器将类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的.编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值但是不能访问.
  2. <clinit>()方法和实例的构造函数()不同,他不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的<clinit>方法已经执行完毕,因此虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object
  3. 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
  4. <clinit>()方法对于对类或者接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法.
  5. 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样会生成()方法.但接口与类不同的是,执行接口<clinit>()不需要先执行父接口<clinit>().只有当父接口中定义的变量被使用时,父接口才会被初始化.另外,接口的实现类在初始化时也一样不会执行接口的()方法.
  6. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕. 如果,在一个类的()方法中有耗时很长的操作,那就很可能造成多个进程阻塞.

<clinit>()方法执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public class NewClass {

static class Parent {
public static int A = 1;
static {
A = 2;
}
}

static class Sub extends Parent {
public static int B = A;
}

public static void main(String[] args) {
System.out.println(Sub.B);
}
}

字段解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DeadLoopClass {

static {
if(true) {
System.out.println(Thread.currentThread() + " init DeadLoopClass ");
while(true){}
}
}

public static void main(String[] args) {
Runnable script = new Runnable() {

@Override
public void run() {
System.out.println(Thread.currentThread() + " start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}

};

Thread t1 = new Thread(script);
Thread t2 = new Thread(script);
t1.start();
t2.start();
}
}

类初始化的四种情况

  1. 遇到new, getstatic, putstatic, invokestatic, 这四条字节码指令时, 如果类没有进行过初始化,则必须先触发初始化
  2. 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化.
  4. 当虚拟机启动的时候,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类.

通过子类引用父类的静态字段,不会导致子类的类初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SuperClass {

static {
System.out.println("SuperClass init");
}

public static int value = 123;


}

class SubClass extends SuperClass {
static {
System.out.println("SubClass init");
}
}

public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}

通过数组定义来引用类,不会触发此类的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SuperClass {

static {
System.out.println("SuperClass init");
}

public static int value = 123;
}

public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}

常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

1
2
3
4
5
6
7
8
9
10
11
12
class ConstClass {
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORLD = "hello world";
}

public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}