类的初始化
## 实例化过程
最近面试的时候遇到很多人都在问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 4 : return public static void main (java.lang.String[]) ; Code: 0 : new #2 3 : dup 4 : invokespecial #3 7 : pop 8 : new #2 11 : dup 12 : invokespecial #3 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 4 : getstatic #2 7 : ldc #3 9 : invokevirtual #4 12 : getstatic #2 15 : ldc #5 17 : invokevirtual #4 20 : return static {}; Code: 0 : getstatic #2 3 : ldc #6 5 : invokevirtual #4 8 : return }
从上面代码的执行结果我们也可以看出, A的代码是先执行的static
静态初始化的(这段代码只有在类被加载进虚拟机中时才会执行一次). 那么我们就先从它分析入手
getstatic
访问java/lang/System.out
这个实例熟悉
ldc
从常量池里加载一个常亮进入操作数栈, 这里加载的是A static init
字符串
invokevirtual
然后调用java/io/PrintStream.println
方法, 输出A static init
字符串
构造器的代码开始执行
aload_0
: 从局部变量表加载一个reference类型值到操作数栈, 这个变量应该是this
invokespecial
: 用于需要特殊处理的实例方法(实例初始化方法, 私有方法和父类方法). 这里是调用A的实例化方法, 也就是{}
这中的代码
getstatic
实例化方法访问java/lang/System.out
属性
ldc
实例化方法从常量池里加载一个常亮进入操作数栈, 这里加载的是A init
字符串
invokevirtual
实例化方法调用java/io/PrintStream.println
方法, 输出A init
字符串
getstatic
构造器访问java/lang/System.out
属性
ldc
构造器从常量池里加载一个常亮进入操作数栈, 这里加载的是A
字符串
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 4 : getstatic #2 7 : ldc #3 9 : invokevirtual #4 12 : getstatic #2 15 : ldc #5 17 : invokevirtual #4 20 : return static {}; Code: 0 : getstatic #2 3 : ldc #6 5 : invokevirtual #4 8 : return }
与A类似, B同样是从类的初始化开始代码执行的
getstatic
访问java/lang/System.out
这个实例熟悉
ldc
从常量池里加载一个常亮进入操作数栈, 这里加载的是B static init
字符串
invokevirtual
然后调用java/io/PrintStream.println
方法, 输出B static init
字符串
然后是构造器方法执行
aload_0
同样的是加载this
进虚拟机栈
invokespecial
调用父类A的实例初始化方法
然后就开死像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>
方法执行过程可能会影响程序运行行为的一些特点和细节
<clinit>
方法由编译器将类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的.编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值但是不能访问.
<clinit>()
方法和实例的构造函数()不同,他不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的<clinit>
方法已经执行完毕,因此虚拟机中第一个被执行的<clinit>()
方法的类肯定是java.lang.Object
由于父类的<clinit>()
方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
<clinit>()
方法对于对类或者接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()
方法.
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样会生成()方法.但接口与类不同的是,执行接口<clinit>()
不需要先执行父接口<clinit>()
.只有当父接口中定义的变量被使用时,父接口才会被初始化.另外,接口的实现类在初始化时也一样不会执行接口的()方法.
虚拟机会保证一个类的<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(); } }
类初始化的四种情况
遇到new, getstatic, putstatic, invokestatic, 这四条字节码指令时, 如果类没有进行过初始化,则必须先触发初始化
使用java.lang.reflect包的方法进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化.
当虚拟机启动的时候,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类.
通过子类引用父类的静态字段,不会导致子类的类初始化
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); } }