Java:HotSpot虚拟机是怎样运行字节码的?

Java:HotSpot虚拟机是怎样运行字节码的?

1、Java为什么在虚拟机里运行?

(1)Java 作为一门高级程序语言,它的语法非常复杂,抽象程度也很高。因此,直接在硬件上运行这种复杂的程序并不现实。
(2)Java的可移植性,Java号称"编写一次,处处运行(Write-Once Run-Anywhere)",这种功能就是基于java的虚拟机实现的。
(3)Java虚拟机提供了一个托管平台,提供内存管理、垃圾回收、编译时动态校验等功能。

2、Java虚拟机是如何运行Java字节码的?

虚拟机角度分析:
(1)编译:JRE通过javac命令将.java文件编译成.class文件;
(2)加载:将编译之后的.class文件加载到内存中,加载后的java类会存放到内存的方法区中,实际运行时虚拟机会运行方法区中的代码;
(3)数据区:虚拟机会划分出堆和栈来存放运行时数据;
(4)栈划分:栈划分为java方法栈和本地方法栈,java方法栈是用来提供栈帧运行区域的;
(5)栈帧:栈帧的大小在编译时就已经确定了,栈帧包含局部变量表、字节码操作数、动态链接和方法出口等,局部变量表用来保存方法中的局部变量数据,java虚拟机不要求栈帧的内存连续;
(6)PC寄存器:线程切换时,通过当前线程的PC寄存器中的数据,确定上次该线程执行到的位置;
(7)方法执行:方法的执行就是栈帧入栈到出栈的过程,当退出方法执行时(正常退出或者抛出异常),java虚拟机都会弹出当前线程的当前栈帧,并将其舍弃。

底层硬件角度分析:
从硬件角度来说,java字节码不可以直接在机器上执行,需要进行一次转换,java虚拟机就是将java字节码翻译成机器码,然后由机器执行操作;
翻译过程有两种:
(1)解释执行:虚拟机执行java字节码时,逐条将字节码翻译成机器码;
(2)即时编译(Just-In-Time compilation):以方法为单位,虚拟机会将热点方法提前进行编译成机器码,然后在需要执行到该方法时只需要执行编译好的机器码集合即可,目前有三种 C1、C2、Graal;
(3)解释执行的优势在于无需等待编译,而即时编译的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。

涉及到的热点代码有两种算法,基于采样的热点探测和基于计数器的热点探测。一般采用的都是基于计数器的热点探测,两者的优缺点请自行查找。基于计数器的热点探测又有两个计数器,方法调用计数器,回边计数器,他们在C1和C2又有不同的阈值。

(4)C1:C1又叫Client编译器,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。
(5)C2:C2又叫Server编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的执行效率较高。
(6)Graal:新一代试验中的即时编译器(后续更新);
(7)运行配比:为了不干扰应用的正常运行,HotSpot 的即时编译是放在额外的编译线程中进行的。HotSpot 会根据 CPU 的数量设置编译线程的数目,并且按 1:2 的比例配置给 C1 及 C2 编译器。

执行效率分析
Java的执行效率也是由编译器所决定的;在解释执行时,Java代码的执行要比C/C++多了一层翻译,即将Java字节码翻译成机器码,而C/C++在编译时就将代码编译成了机器码,但从效率上而言Java不如C/C++;可也有例外,在Java引入即时编译器之后,即时编译拥有程序的运行时信息,并且能够分局运行时信息做出响应的优化。举例:我们知道虚方法是用来实现面向对象语言多态性的。对于一个虚方法调用,尽管它有很多个目标方法,但在实际运行过程中它可能只调用其中的一个。这个信息便可以被即时编译器所利用,来规避虚方法调用的开销,从而达到比静态编译的 C++ 程序更高的性能。

3、Java的基本数据类型

Java基本数据类型有8个: boolean(1)、char(2)、short(2)、int(4)、long(8)、float(4)、double(8);

基本数据类型在栈帧的数据存储格式与栈/堆中的存储格式不同,举例:Win系统有32位和64位,Hotspot也有32位和64位的,其实说的就是栈帧的基本数据单元是32位(4字节)还是64位(8字节);32位的虚拟机将long和double分成两个数据单元存储,在线程切换时可能会遇到操作一个long或double对象时线程就切换了;

数据填充:
基本数据类型在栈帧中都是以数字存储的,boolean java默认规范是0代表false、1代表true;而在栈帧中boolean会强制存储成int型,对于 Java 虚拟机来说,它看到的 boolean 类型,早已被映射为整数类型。因此,将原本声明为 boolean 类型的局部变量,赋值为除了 0、1 之外的整数值,在 Java 虚拟机看来是“合法”的。
对于 boolean、char 这两个无符号类型来说,加载伴随着零扩展。举个例子,char 的大小为两个字节。在加载时 char 的值会被复制到 int 类型的低二字节,而高二字节则会用 0 来填充。


import sun.misc.Unsafe;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class Foo {
     

    private boolean flag = true;

    public boolean getFlag() {
     

        return this.flag;
    }

    public static void main(String[] args) throws NoSuchFieldException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
     

        Foo foo = new Foo();
        Field field = foo.getClass().getDeclaredField("flag");
        Unsafe unsafe = getUnsafeByConstructor();
        long addr = unsafe.objectFieldOffset(field);

        for (byte b = 0; b < 4; b++) {
     
            System.out.println("Unsafe.putByte: " + b);
            unsafe.putByte(foo, addr, b);

            System.out.println("Unsafe.getByte: " + unsafe.getByte(foo, addr));
            System.out.println("Unsafe.getBoolean: " + unsafe.getBoolean(foo, addr));

            if (foo.flag) {
     
                System.out.println("Hello Java 1 flag");
            }

            // true == 1
            if (true == foo.flag) {
     
                System.out.println("Hello Java 2 flag");
            }

            if (foo.getFlag()) {
     
                System.out.println("Hello Java 1 getFlag");
            }

            if (true == foo.getFlag()) {
     
                System.out.println("Hello Java 2 getFlag");
            }

            System.out.println("--------------------------------");
        }
    }

    private static Unsafe getUnsafeByConstructor() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
     

        Constructor<Unsafe> unsafeConstructor = Unsafe.class.getDeclaredConstructor();
        unsafeConstructor.setAccessible(true);
        Unsafe unsafe = unsafeConstructor.newInstance();
        return unsafe;
    }

    private static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
     

        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        return unsafe;
    }
}

下面试以上代码的运行结果:

Unsafe.putByte: 0
Unsafe.getByte: 0
Unsafe.getBoolean: false
--------------------------------
Unsafe.putByte: 1
Unsafe.getByte: 1
Unsafe.getBoolean: true
Hello Java 1 flag
Hello Java 2 flag
Hello Java 1 getFlag
Hello Java 2 getFlag
--------------------------------
Unsafe.putByte: 2
Unsafe.getByte: 2
Unsafe.getBoolean: true
Hello Java 1 flag
--------------------------------
Unsafe.putByte: 3
Unsafe.getByte: 3
Unsafe.getBoolean: true
Hello Java 1 flag
Hello Java 1 getFlag
Hello Java 2 getFlag
--------------------------------

java的Unsafe类是单例的,根据地址可以直接操作内存数据;
下面代码时通过unsafe对象获取foo对象flag属性地址

long addr = unsafe.objectFieldOffset(field);

下面的这行代码是通过unsafe对象修改foo对象flag属性的值,b可以为int范围内的任何值;

unsafe.putByte(foo, addr, b);

运行结果中当b为0时,所有输出都没有结果;当b为1时,所有输出都有结果;当b为2时,与true的判等操作就没输出,那是因为虚拟机默认true的值为1,而此时flag的值为2,所以判等失败;而在if(flag)判断中是判断flag的值不为0即可;

参考:
深入拆解java虚拟机
东方大佬的例子

侵权请留言删除

你可能感兴趣的:(java底层,java,编译器,c++,hotspot)