JVM 编译的细节
Java中boolean类型
首先先来查看如下代码:
public class BooleanTest {
public static void main(String []args){
boolean ok = true;
if(ok){
System.out.println("hello ok");
}
if(ok == true){
System.out.println("ok is true");
}
}
}
如果运行该代码,很容易能够看出来结果应该是
hello ok
ok is true
本身它并没有多大意义,在Java看来这者应该是一样的,但是Java的底层也是这么认为的吗?那可能并不是这样,可以做一个关于boolean类型的测试。首先需要一个能够修改字节码的工具,asmtools,可以直接在网上下载,如果没有,可以从下面的链接直接下载:
https://github.com/dwtfukgv/asmtools.git
然后需要对该Java类的字节码进行修改,修改操作如下:
javac BooleanTest.java
java -cp ./asmtools.jar org.openjdk.asmtools.jdis.Main BooleanTest.class > BooleanTest.jasm.temp
awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' BooleanTest.jasm.temp > BooleanTest.jasm
java -cp ./asmtools.jar org.openjdk.asmtools.jasm.Main BooleanTest.jasm
再次执行BooleanTest的字节码文件
java BooleanTest
可以看出输出结果:
hello ok
并没有"ok is true"这一行数据了,并且程序能够正常运行,这是为什么呢?
首先先解释一下上面的字节的修改操作,主要操作就是将BooleanTest.class中的第一个iconst_1替换为iconst_2,这两个字节串的意思就是常量1和常量2,也就说,在原来的Java程序中,ok赋值为1,同时"ok is true"能够输出,然后把ok赋值为2后,"ok is true"就不能输出了,可以推导出在Java中true为1。可以查看一下BooleanTest的asm文件的内容:
cat BooleanTest.jasm.temp
内容输出如下:
super public class BooleanTest
version 52:0
{
public Method "<init>":"()V"
stack 1 locals 1
{
aload_0;
invokespecial Method java/lang/Object."<init>":"()V";
return;
}
public static Method main:"([Ljava/lang/String;)V"
stack 2 locals 2
{
iconst_1;
istore_1;
iload_1;
ifeq L14;
getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
ldc String "hello ok";
invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
L14: stack_frame_type append;
locals_map int;
iload_1;
iconst_1;
if_icmpne L27;
getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
ldc String "ok is true";
invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
L27: stack_frame_type same;
return;
}
} // end Class BooleanTest
JVM类的加载过程简述
类的加载过程主要分成三步,依次是加载、链接、初始化。
类的加载
加载,是指查找字节流,并且据此创建类的过程,这里的字节流可以是本地字节流,也就是由Java编译器生成的class文件,也可以是从网络中获取字节流。对于数组类来说,它并没有对应的字节流,而是由Java虚拟机直接生成的。对于其他的类来说,Java虚拟机则需要借助类加载器来完成查找字节流的过程。Java提供三种类加载器,启动类加载器(Bootstrap)、扩展类加载器(Extension)、应用类加载器(application)。
启动类加载器负责加载最为基础、最为重要的类,比如存放在JRE的lib目录下jar包中的类(以及由虚拟机参数-Xbootclasspath指定的类)。
扩展类加载器的父类加载器是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在JRE的lib/ext目录下jar包中的类(以及由系统变量java.ext.dirs指定的类)。
应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数-cp/-classpath、系统变量java.class.path或环境变量CLASSPATH所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
类的链接
链接,是指将创建成的类合并至Java虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。
- 验证阶段的目的,在于确保被加载类能够满足Java虚拟机的约束条件。
- 准备阶段的目的,则是为被加载类的静态字段分配内存。Java代码中对静态字段的具体初始化,这里的初始化是指初始化为默认值而不是具体值,部分Java虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。在class文件被加载至Java虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。
- 解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
类的初始化
如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。
如果直接赋值的静态字段被final所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被Java编译器标记成常量值,其初始化直接由Java虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被Java编译器置于同一方法中,并把它命名为< clinit >。
类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行< clinit >方法的过程。Java虚拟机会通过加锁来确保类的< clinit >方法仅被执行一次。只有当初始化完成之后,类才正式成为可执行的状态。
那么,类的初始化何时会被触发呢?JVM规范枚举了下述多种触发情况:
- 当虚拟机启动时,初始化用户指定的主类;
- 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射API对某个类进行反射调用时,初始化这个类;
- 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
JVM类加载实例分析
首先看如下代码:
public class Singleton {
private Singleton() {}
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
static {
System.out.println("LazyHolder init...");
}
}
public static Object getInstance(boolean ok) {
if (ok) return new LazyHolder[2];
return LazyHolder.INSTANCE;
}
public static void main(String[] args) {
getInstance(true);
System.out.println("----------------------------");
getInstance(false);
}
}
问题在于代码中的第11行,也就是新建数组那一行,会导致类LazyHolder的加载,链接和初始化吗?
对于类的加载时间,可以通过日志来看,使用如下命令:
javac Singleton.java
java -verbose:class Singleton
可能会出现如下结果,结果只截取了后面几行,没有全部截取:
[Loaded Singleton from file:/Users/dwtfukgv/Documents/script/Cpp/java/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded Singleton$LazyHolder from file:/Users/dwtfukgv/Documents/script/Cpp/java/]
----------------------------
LazyHolder init...
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
从上面可以看出来,类LazyHolder在第5行加载了,第6行是主函数的输出,第7行才是类LazyHolder初始始化的输出,所以新建数组能够导致类加载,不能够导致类进行初始化,那么是否能够导致类的链接呢?下面再做一个实验,进行如下操作:
java -cp ./asmtools.jar org.openjdk.asmtools.jdis.Main Singleton$LazyHolder.class > Singleton$LazyHolder.jasm.temp
awk 'NR==1,/stack 1/{sub(/stack 1/, "stack 0")} 1' Singleton$LazyHolder.jasm.temp > Singleton$LazyHolder.jasm # 将构造方法的栈大小从1变成0
java -cp ./asmtools.jar org.openjdk.asmtools.jasm.Main Singleton$LazyHolder.jasm
java -verbose:class Singleton
可能会出现如下结果,结果只截取了后面几行,没有全部截取:
[Loaded Singleton from file:/Users/dwtfukgv/Documents/script/Cpp/java/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.VerifyError from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.NoSuchMethodException from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" [Loaded java.lang.Throwable$PrintStreamOrWriter from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Throwable$WrappedPrintStream from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.IdentityHashMap from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.IdentityHashMap$KeySet from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
java.lang.VerifyError: Operand stack overflow
Exception Details:
Location:
Singleton.<init>()V @0: aload_0
Reason:
Exceeded max stack size.
Current Frame:
bci: @0
flags: { flagThisUninit }
locals: { uninitializedThis }
stack: { }
Bytecode:
0x0000000: 2ab7 0008 b1
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
at java.lang.Class.getMethod0(Class.java:3018)
at java.lang.Class.getMethod(Class.java:1784)
at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:650)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:632)
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar]
从上述输出来看,第一行加载完成了类Singleton,后面出错了,并且没有看到类LazyHolder的加载完成的日志,所以类LazyHolder没有完成类加载整个过程。并且根据前一个例子可以知道,在新建数组时,肯定会经过类的加载,但不会经过初始化,并且可以想到类的链接的第一个阶段就是验证,刚才修改了类的字节码文件,把构造方法的栈大小设置为0,原来为1,导致了栈溢出,所以经过了类的链接过程,就是在链接中的验证阶段出现错误。
所以可以得到结论,在新建数组时类会进行加载和链接过程,但并不会进行初始化操作。并且还可以看出,在new一个对象时,会执行类的加载、链接和初始化的全部操作。