JVM 编译的细节

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规范枚举了下述多种触发情况:

  1. 当虚拟机启动时,初始化用户指定的主类;
  2. 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;
  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
  5. 子类的初始化会触发父类的初始化;
  6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  7. 使用反射API对某个类进行反射调用时,初始化这个类;
  8. 当初次调用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一个对象时,会执行类的加载、链接和初始化的全部操作。

原文地址:https://www.cnblogs.com/dwtfukgv/p/14865881.html