当你在 Java 程序中new
对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同窗相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,而后给出几个易出错的实例来分析,帮助你们更好理解这个知识点。javascript
JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization。html
下面分别介绍这三个过程:java
Loading 过程主要工做是由ClassLoader
完成。该过程具体包括三件事:mysql
Class
对象的实例来表示该类JVM 中除了最顶层的Boostrap ClassLoader
是用 C/C++ 实现外,其他类加载器均由 Java 实现,咱们能够用getClassLoader
方法来获取当前类的类加载器:sql
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println(ClassLoaderDemo.class.getClassLoader());
}
}
# sun.misc.Launcher$AppClassLoader@30a4effe
# AppClassLoader 也就是上图中的 System Class Loader复制代码
此外,咱们在启动java
传入-verbose:class
来查看加载的类有那些。数据结构
java -verbose:class ClassLoaderDemo
[Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
....
....
[Loaded java.security.BasicPermissionCollection from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded ClassLoaderDemo from file:/Users/liujiacai/codes/IdeaProjects/mysql-test/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
sun.misc.Launcher$AppClassLoader@2a139a55
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]复制代码
Verification
主要是保证类符合 Java 语法规范,确保不会影响 JVM 的运行。包括但不限于如下事项:oracle
final
类没有被继承,final
方法没有被覆盖在一个类已经被load
而且经过verification
后,就进入到preparation
阶段。在这个阶段,JVM 会为类的成员变量分配内存空间而且赋予默认初始值,须要注意的是这个阶段不会执行任何代码,而只是根据变量类型
决定初始值。若是不进行默认初始化,分配的空间的值是随机的,有点类型c语言中的野指针问题。dom
Type Initial Value
int 0
long 0L
short (short) 0
char '\u0000'
byte (byte) 0
boolean false
reference null
float 0.0f
double 0.0d复制代码
在这个阶段,JVM 也可能会为有助于提升程序性能的数据结构分配内存,常见的一个称为method table
的数据结构,它包含了指向全部类方法(也包括也从父类继承的方法)的指针,这样再调用父类方法时就不用再去搜索了。jvm
Resolution
阶段主要工做是确认类、接口、属性和方法在类run-time constant pool
的位置,而且把这些符号引用(symbolic references)替换为直接引用(direct references)。ide
locating classes, interfaces, fields, and methods referenced symbolically from a type's constant pool, and replacing those symbolic references with direct references.
这个过程不是必须的,也能够发生在第一次使用某个符号引用时。
通过了上面的load
、link
后,第一次
主动调用
某类的最后一步是Initialization
,这个过程会去按照代码书写顺序进行初始化,这个阶段会去真正执行代码,注意包括:代码块(static与static)、构造函数、变量显式赋值。若是一个类有父类,会先去执行父类的initialization
阶段,而后在执行本身的。
上面这段话有两个关键词:第一次
与主动调用
。第一次
是说只在第一次时才会有初始化过程,之后就不须要了,能够理解为每一个类有且仅有一次
初始化的机会。那么什么是主动调用
呢?
JVM 规定了如下六种状况为主动调用
,其他的皆为被动调用
:
new
操做、反射、cloning
,反序列化)static
方法static
属性进行赋值时(这不包括final
的与在编译期肯定的常量表达式)main
方法的类)本文后面会给出一个示例用于说明主动调用
的被动调用
区别。
在这个阶段,执行代码的顺序遵循如下两个原则:
class Singleton {
private static Singleton mInstance = new Singleton();// 位置1
public static int counter1;
public static int counter2 = 0;
// private static Singleton mInstance = new Singleton();// 位置2
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstantce() {
return mInstance;
}
}
public class InitDemo {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstantce();
System.out.println("counter1: " + singleton.counter1);
System.out.println("counter2: " + singleton.counter2);
}
}复制代码
当mInstance
在位置1时,打印出
counter1: 1
counter2: 0复制代码
当mInstance
在位置2时,打印出
counter1: 1
counter2: 1复制代码
Singleton
中的三个属性在Preparation
阶段会根据类型赋予默认值,在Initialization
阶段会根据显示赋值的表达式再次进行赋值(按顺序自上而下执行)。根据这两点,就不难理解上面的结果了。
class NewParent {
static int hoursOfSleep = (int) (Math.random() * 3.0);
static {
System.out.println("NewParent was initialized.");
}
}
class NewbornBaby extends NewParent {
static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);
static {
System.out.println("NewbornBaby was initialized.");
}
}
public class ActiveUsageDemo {
// Invoking main() is an active use of ActiveUsageDemo
public static void main(String[] args) {
// Using hoursOfSleep is an active use of NewParent,
// but a passive use of NewbornBaby
System.out.println(NewbornBaby.hoursOfSleep);
}
static {
System.out.println("ActiveUsageDemo was initialized.");
}
}复制代码
上面的程序最终输出:
ActiveUsageDemo was initialized.
NewParent was initialized.
1复制代码
之因此没有输出NewbornBaby was initialized.
是由于没有主动去调用NewbornBaby
,若是把打印的内容改成NewbornBaby.hoursOfCrying
那么这时就是主动调用NewbornBaby
了,相应的语句也会打印出来。
public class Alibaba {
public static int k = 0;
public static Alibaba t1 = new Alibaba("t1");
public static Alibaba t2 = new Alibaba("t2");
public static int i = print("i");
public static int n = 99;
private int a = 0;
public int j = print("j");
{
print("构造块");
}
static {
print("静态块");
}
public Alibaba(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++i;
++n;
}
public static int print(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String args[]) {
Alibaba t = new Alibaba("init");
}
}复制代码
上面这个例子是阿里巴巴在14年的校招附加题,我当时看到这个题,就以为与阿里无缘了。囧
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99
9:j i=8 n=100
10:构造块 i=9 n=101
11:init i=10 n=102复制代码
上面是程序的输出结果,下面我来一行行分析之。
Alibaba
是 JVM 的启动类,属于主动调用,因此会依此进行 loading、linking、initialization 三个过程。k
第一个显式赋值为 0 。接下来是t1
属性,因为这时Alibaba
这个类已经处于 initialization 阶段,static 变量无需再次初始化了,因此忽略 static 属性的赋值,只对非 static 的属性进行赋值,全部有了开始的:
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2复制代码
接着对t2
进行赋值,过程与t1相同
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5复制代码
以后到了 static 的 i
与 n
:
7:i i=6 n=6复制代码
到如今为止,全部的static的成员变量已经赋值完成,接下来就到了 static 代码块
8:静态块 i=7 n=99复制代码
至此,全部的 static 部分赋值完毕,接下来是非 static 的 j
9:j i=8 n=100复制代码
全部属性都赋值完毕,最后是构造块与构造函数
10:构造块 i=9 n=101
11:init i=10 n=102复制代码
通过上面这9步,Alibaba
这个类的初始化过程就算完成了。这里面比较容易出错的是第3步,认为会再次初始化 static 变量或代码块。而其实是不必,不然会出现屡次初始化的状况。
但愿你们能多思考思考这个例子的结果,加深这三个过程的理解。
通过最后这三个例子,相信你们对 JVM 对类加载机制都有了更深的理解,若是你们仍是有疑问,欢迎留意讨论。