在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的
理解: public static int number = 666;
上面这段代码,在类加载的连接阶段,为对象number分配内存,并初始化为0;然后再初始化阶段在赋予正确的初始值:666
Java程序对类的使用方式可分为两种
所有的Java虚拟机实现必须在每个类或接口被Java程序“ 首次主动使用 ”时才初始化他们
代码一
public class Test01 { public static void main(String[] args) { System.out.println(Child01.str); } } class Father01 { public static String str = "做一个好人!"; static { System.out.println("Father01 static block"); } } class Child01 extends Father01 { static { System.out.println("Child01 static block"); } }
运行结果做一个好人!
Father01 static block 做一个好人!
代码二
public class Test01 { public static void main(String[] args) { System.out.println(Child01.str2); } } class Father01 { public static String str = "做一个好人!"; static { System.out.println("Father01 static block"); } } class Child01 extends Father01 { public static String str2 = "做一个好人!"; static { System.out.println("Child01 static block"); } }
运行结果
Father01 static block Child01 static block 做一个好人!
分析:
以上验证的是类的初始化情况,那么如何验证类的加载情况呢,可以通过在启动的时候配置虚拟机参数: -XX:+TraceClassLoading
查看
运行代码一,查看输出结果,可以看见控制台打印了very多的日志,第一个加载的是 java.lang.Object
类(不管加载哪个类,他的父类一定是Object类),后面是加载的一系列jdk的类,他们都位于rt包下。往下查看,可以看见 Loaded classloader.Child01
,说明即使没有初始化Child01,但是程序依然加载了Child01类。
[Opened /usr/local/jdk1.8/jre/lib/rt.jar] [Loaded java.lang.Object from /usr/local/jdk1.8/jre/lib/rt.jar] ... [Loaded java.lang.Void from /usr/local/jdk1.8/jre/lib/rt.jar] [Loaded classloader.Father01 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/] [Loaded classloader.Child01 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/] Father01 static block 做一个好人! [Loaded java.lang.Shutdown from /usr/local/jdk1.8/jre/lib/rt.jar] [Loaded java.lang.Shutdown$Lock from /usr/local/jdk1.8/jre/lib/rt.jar]
因为前一章节使用了JVM参数,所以对其做一下简单的介绍
-XX: -XX:+<option> -XX:-<option> -XX:<option>=<value>
public class Test02 { public static void main(String[] args) { System.out.println(Father02.str); } } class Father02{ public static final String str = "做一个好人!"; static { System.out.println("Father02 static block"); } }
执行结果
做一个好人!
分析
可以看见,此段代码并没有初始化Father02类。这是因为final表示的是一个常量,在编译阶段常量就会被存入到调用这个常量的方法所在的类的常量池当中,本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。在本代码中,常量str会被存入到Test02的常量池中,之后Test02与Father02没有任何关系,甚至可以删除Father02的class文件。
我们反编译一下Test02类
Compiled from "Test02.java" public class classloader.Test02 { public classloader.Test02(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String 做一个好人! 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return }
第一块是Test02类的构造方法,第二块是我们要看的main方法。可以看见 3: ldc #4 // String 做一个好人!
,此时这个值已经是确定无疑的 做一个好人!
了,而不是 Father02.str
,证实了上面说的 在编译阶段常量就会被存入到调用这个常量的方法所在的类的常量池当中
。
因前一章节涉及到了助记符,所以介绍下本章节涉及到的助记符及扩展
public class Test03 { public static void main(String[] args) { System.out.println(Father03.str); } } class Father03 { public static final String str = UUID.randomUUID().toString(); static { System.out.println("Father03 static block"); } }
运行结果
Father03 static block a60c5db4-2673-4ffc-a9f0-2dbe53fae583
分析
本代码与示例二的区别在于 str
的值是在运行时确认的,而不是编译时就确定好的,属于运行期常量,而不是编译期常量。当一个常量的值并非编译期间确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,导致这个类被初始化。
代码一
public class Test04 { public static void main(String[] args) { Father04 father04_1 = new Father04(); System.out.println("-----------"); Father04 father04_2 = new Father04(); } } class Father04 { static { System.out.println("Father04 static block"); } }
运行结果
Father04 static block -----------
分析
代码二
public class Test04 { public static void main(String[] args) { Father04[] father04s = new Father04[1]; System.out.println(father04s.getClass()); } }
运行结果
class [Lclassloader.Father04;
分析
[Lclassloader.Father04
,这是虚拟机在运行期生成的。 -> 对于数组示例来说,其类型是有JVM在运行期动态生成的,表示为 [Lclassloader.Father04
这种形式,动态生成的类型,其父类就是Object。 反编译一下:
public static void main(java.lang.String[]); Code: 0: iconst_1 1: anewarray #2 // class classloader/Father04 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: return
代码一
public class Test05 { public static void main(String[] args) { System.out.println(Child05.j); } } interface Father05 { int i = 5; } interface Child05 extends Father05 { int j = 6; }
运行结果
分析
代码二
public class Test05 { public static void main(String[] args) { System.out.println(Child05.j); } } interface Father05 { int i = 5; } interface Child05 extends Father05 { int j = new Random().nextInt(8); }
运行结果
将Father05.class文件删除,运行结果
Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Father05 at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) at java.net.URLClassLoader.access$100(URLClassLoader.java:73) at java.net.URLClassLoader$1.run(URLClassLoader.java:368) at java.net.URLClassLoader$1.run(URLClassLoader.java:362) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:361) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at classloader.Test05.main(Test05.java:15) Caused by: java.lang.ClassNotFoundException: classloader.Father05 at java.net.URLClassLoader.findClass(URLClassLoader.java:381) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 13 more
分析
代码三
public class Test05 { public static void main(String[] args) { System.out.println(Child05.j); } } interface Father05 { Thread thread = new Thread() { { System.out.println("Father05 code block"); } }; } class Child05 implements Father05 { public static int j = 8; }
运行结果
分析
代码四
public class Test05 { public static void main(String[] args) { System.out.println(Father05.thread); } } interface GrandFather { Thread thread = new Thread() { { System.out.println("GrandFather code block"); } }; } interface Father05 extends GrandFather{ Thread thread = new Thread() { { System.out.println("Father05 code block"); } }; }
运行结果
Father05 code block Thread[Thread-0,5,main]
分析
代码一
public class Test06 { public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); System.out.println("i:" + Singleton.i); System.out.println("j:" + Singleton.j); } } class Singleton { public static int i; public static int j = 0; private static Singleton singleton = new Singleton(); private Singleton() { i ++; j ++; } public static Singleton getInstance() { return singleton; } }
运行结果
i:1 j:1
分析
首先 Singleton.getInstance();
进入 Singleton
的 getInstance
方法, getInstance
会返回 Singleton
的实例, Singleton
的实例是 new Singleton();
出来的,因此调用了自定义的私有构造方法。在调用构造方法之前,给静态变量赋值, i
默认赋值为0, j
显式的赋值为0,经过构造函数之后,值都为1。
代码二
public class Test06 { public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); System.out.println("i:" + Singleton.i); System.out.println("j:" + Singleton.j); } } class Singleton { public static int i; private static Singleton singleton = new Singleton(); private Singleton() { i ++; j ++; } public static int j = 0; public static Singleton getInstance() { return singleton; } }
运行结果
i:1 j:0
分析
程序主动使用了Singleton类,准备阶段对类的 静态变量 分配内存,赋予 默认值 ,下面给出类在连接及初始化阶段常量的值的变化
故返回的值i为1,j为0
类被加载后,就进入连接阶段。连接就是将已经读入到内存中的类的二进制数据合并到虚拟机的运行时环境中去
类的验证的内容
在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于下面的Sample类,在准备阶段,将为int类型的静态变量 i
分配4个字节的内存空间,并且赋默认值0;为long类型的静态变量j分配8个字节的内存空间,并赋予默认值0
public class Sample { private static int i = 8; private static long j = 8L; ...... }
在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:
静态变量的声明语句,预计静态代码块都被看作类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行他们
当Java虚拟机初始化一个类时,要求他的所有父类都已经被初始化,但是这条规则并不适用于接口
因此,一个父接口并不会因为他的子接口或实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。代码参照代码理解-接口的初始化
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
类的生命周期除了前文提到的加载、连接、初始化之外,还有类示例化,垃圾回收和对象终结
<init>
。针对源代码中每一个类的构造方法,Java编译器都产生一个 <init>
方法