来看下面的代码,我们通过MyParent4这个类来创建数组,看看是否会执行MyParent4的静态代码块:
package com.leolin.jvm; public class MyTest4 { public static void main(String[] args) { MyParent4[] myParent4s = new MyParent4[1]; System.out.println(myParent4s.getClass()); } } class MyParent4 { static { System.out.println("MyParent4 static block"); } }
运行上面的代码,会有如下输出:
class [Lcom.leolin.jvm.MyParent4;
可以看出,程序并没有对MyParent4进行初始化,因为我们声明一个MyParent4的数组,并不代表着对MyParent4的主动使用,[Lcom.leolin.jvm.MyParent4代表着MyParent4数组在JVM内部的类型,这个类型是运行期生成的。
我们将MyTest4的代码修改如下,这次,我们多生成一个MyParent4的二维数组和一个int类型的一维数组:
package com.leolin.jvm; public class MyTest4 { public static void main(String[] args) { MyParent4[] myParent4s = new MyParent4[1]; System.out.println(myParent4s.getClass()); MyParent4[][] myParent4s1 = new MyParent4[1][1]; System.out.println(myParent4s1.getClass()); int[] ints = new int[1]; System.out.println(ints.getClass()); } } class MyParent4 { static { System.out.println("MyParent4 static block"); } }
运行结果如下:
class [Lcom.leolin.jvm.MyParent4; class [[Lcom.leolin.jvm.MyParent4; class [I
可以知道,不论我们是用MyParent4生成一维数组还是二维,都不会导致MyParent4的初始化。另外我们可以看到,符号[代表一维的数组类型,如果是二维即是[[。这里,我们反编译MyTest4文件:
D:Fworkjava_spacejvm-lecture argetclassescomleolinjvm>javap -c MyTest4.class Compiled from "MyTest4.java" public class com.leolin.jvm.MyTest4 { public com.leolin.jvm.MyTest4(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_1 1: anewarray #2 // class com/leolin/jvm/MyParent4 4: astore_1 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 8: aload_1 9: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class; 12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 15: iconst_1 16: iconst_1 17: multianewarray #6, 2 // class "[[Lcom/leolin/jvm/MyParent4;" 21: astore_2 22: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 25: aload_2 26: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class; 29: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 32: iconst_1 33: newarray int 35: astore_3 36: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 39: aload_3 40: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class; 43: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 46: return }
我们看主方法的第一行、第十七行和第三十三行的助记符anewarray、multianewarray和newarray,这三行分别对应生成MyParent4一维数组、MyParent4二维数组和int一维数组。我们来详细说下这三个助记符的作用:
- anewarray:表示创建一个引用类型(如类、接口)的数组,并将其引用值压入栈顶。
- multianewarray:表示创建一个多维数组,并将其引用值压入栈顶。多维数组就是数组里面还包含数组,也可以通过重复使用anewarray和newarray来进行分配。multianewarray指令只不过把创建多维数组的指令所需要的字节码压缩到一条指令中。
- newarray:表示创建一个指定原始类型(int、boolean、float、char)的数组,并将其引用值压入栈顶。
之前我们证明了,针对于类,子类的初始化必定要先完成父类的初始化,那么接口呢?我们来看下面这个例子:
package com.leolin.jvm; import java.util.Random; public class MyTest5 { public static void main(String[] args) { System.out.println(MyChild5.b); } } interface MyParent5 { Thread threadParent = new Thread() { { System.out.println("MParent5 invoke"); } }; } interface MyChild5 extends MyParent5 { Thread threadChild = new Thread() { { System.out.println("MyChild5 invoke"); } }; int b = new Random().nextInt(3); }
上面接口代码省略了static和final,因为只要是在接口里声明的常量,默认就是静态常量,MyChild5中的静态常量b,它的值必须在运行期才能确定,所以我们调用MyChild5的常量b,必定引发MyChild5的初始化。同时,由于接口不允许编写静态代码块,所以我们分别在MyChild5和MyParent5声明了两个Thread类型的静态常量threadChild和threadParent,并在这两个声明中加入构造代码块,在构造代码块中加入打印各自的代码输出,构造代码在每次实例化的时候,多会执行一次。
运行上面的代码,得到如下的输出:
MyChild5 invoke 2
threadChild中的代码块打印了,而MyChild5继承MyParent5,但MyParent5中的代码块并没有执行。因此,相对于接口,子接口的初始化,并不要求父接口初始化完毕。
那么,如果一个类实现了一个接口,类的初始化,是否会导致接口的初始化呢?我们来看下面的例子:
package com.leolin.jvm; public class MyTest5 { public static void main(String[] args) { MyChild5 myChild5 = new MyChild5(); myChild5.printHello(); } } interface MyParent5 { Thread threadParent = new Thread() { { System.out.println("MParent5 invoke"); } }; void printHello(); } class MyChild5 implements MyParent5 { static { System.out.println("MyChild5 code block"); } @Override public void printHello() { System.out.println("hello"); } }
执行上面的代码,得到如下输出:
MyChild5 code block hello
MyParent5中的threadParent的构造代码块依旧没有执行,所以类的初始化,并不要求接口也初始化。另外,没有初始化,并不代表MyParent5没有被加载,配置-XX:+TraceClassLoading选项,打印输出:
…… [Loaded com.leolin.jvm.MyTest5 from file:/D:/F/work/java_space/jvm-lecture/target/classes/] …… [Loaded com.leolin.jvm.MyParent5 from file:/D:/F/work/java_space/jvm-lecture/target/classes/] [Loaded com.leolin.jvm.MyChild5 from file:/D:/F/work/java_space/jvm-lecture/target/classes/] [Loaded com.leolin.jvm.MyParent5$1 from file:/D:/F/work/java_space/jvm-lecture/target/classes/] MyChild5 code block hello ……
可以看到,程序是有加载MyParent5。
我们来看下面这个例子,当我们调整静态变量声明的位置,可能会导致不一样的运行结果:
package com.leolin.jvm; public class MyTest6 { public static void main(String[] args) { Singleton singleton = Singleton.getSingleton(); System.out.println("counter1:" + Singleton.counter1); System.out.println("counter2:" + Singleton.counter2); } } class Singleton { public static int counter1; public static int counter2 = 0; private static Singleton singleton = new Singleton(); private Singleton() { counter1++; counter2++; } public static Singleton getSingleton() { return singleton; } }
这段代码的运行结果为:
counter1:1 counter2:1
现在,我们修改Singleton的静态变量声明顺序:
class Singleton { public static int counter1; private static Singleton singleton = new Singleton(); private Singleton() { counter1++; counter2++; } public static int counter2 = 0; public static Singleton getSingleton() { return singleton; } }
重新执行代码:
counter1:1 counter2:0
可以看到仅仅是因为声明顺序的改变,导致了counter2的结果发生了变化。我们主动使用了Singleton了,所以Singleton会有加载、连接、初始化这三个阶段。再连接这个阶段中,会给静态变量分配内存,赋予默认值。int类型的默认值为0,Singleton类型的默认值为null。而调用类的静态方法会导致类的初始化,开始从上到下的赋初值行为,counter1没有初值,而singleton在执行私有构造方法后,counter1为1,counter2为1。只是执行完构造方法后,counter2又被赋值为0,覆盖了原来的1。
类初始化
- 为新的对象分配内存
- 为实例变量赋默认值
- 为实例变量赋正确的初始值
Java编译器在它编译的每一个类都至少生成一个实例初始化方法,在Java的class文件中,这个实例化方法被称为<init>。针对源代码中每一个类的构造方法,java编译器都会产生一个<init>方法。
类的加载
- 类的加载的最终产品是位于内存中的Class对象
- Class对象封装了类在方法区内的数据结构,并向Java程序员提供了访问方法区内的数据结构的接口