zoukankan      html  css  js  c++  java
  • 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一个对象时,会执行类的加载、链接和初始化的全部操作。

  • 相关阅读:
    2017-3-28 javaScript DOM 操作
    2017-3-28 JavaScript 基础、语法
    2017-3-25 CSS 样式表(二)
    js判断文件是否存在的方法
    关于eclipse添加自动查找文件以及svn的插件
    关于jqgrid的一些使用
    ztree框架使用问题汇总
    bootstrap框架的使用
    UIButton 设置阴影效果
    xcode升级至9.0之后,新建xib报错: Safe Area Layout Guide Before IOS 9.0
  • 原文地址:https://www.cnblogs.com/dwtfukgv/p/14865881.html
Copyright © 2011-2022 走看看