深入探索JVM垃圾回收:ARM服务器垃圾回收的挑战和优化
上QQ阅读APP看书,第一时间看更新

1.7 扩展阅读:JIT概述

虚拟机的实现通常可以划分为3部分:运行时(Run-Time)、编译优化(JIT)和垃圾回收。已经有较多的书籍和文章介绍了运行时,本书不再介绍。垃圾回收是本书的重点,后面会详细介绍。关于JIT的相关介绍并不多,同时JIT也非常复杂,特别是编译优化的相关知识。本节在Linux/AArch64平台的基础上,通过一个简单的例子演示JIT的基本概念。

首先从一个简单的C代码例子出发,如下所示:

#include <stdio.h>
int add(int a, int b){
    return a + b;
}
int main(){
    printf("%d\n",add(4,5));
    return 0;
}

该代码片段的功能非常简单,其中函数add实现加法功能。这个add例子和1.4.1节中Java的add功能完全相关,都是完成两个整数的加法计算并返回结果。本节构造C的add函数就是为了让读者可以方便地理解在编译优化时Java的函数(字节码片段)可以被一个C/C++的函数替代。当然,这里省略了JVM构造这个C语言的add函数的过程,这本质上就是编译优化要做的工作。

使用gcc进行编译,这里先使用O2的编译优化级别,命令如下:

gcc -O2 -o test test.c

编译后使用objdump命令查看add函数的反汇编代码:

0000000000400650 <add>:
  400650:       0b010000        add     w0, w0, w1
  400654:       d65f03c0        ret

注意

在AArch64平台中有31个通用寄存器,其中x0~x7用于传递参数和返回值。w0~w7是x0~x7的低32位,用于传递32位的参数,当函数的参数个数超过8个时,通过栈传递。

在这个例子中,add的两个参数通过w0和w1传入,通过add指令完成加法,结果存放在寄存器w0中,通过ret返回函数的执行结果。

假设JVM识别Java的add函数为热点,现在也知道add函数对应的汇编代码,那么还有一个问题,就是如何让JVM替换原来的add函数而执行编译后的代码。下面通过一个例子演示C/C++代码直接执行编译后代码的过程。首先将编译后的代码作为输入数据,表示待执行的函数,然后通过mmap函数将数据加载到内存区,并设置内存区可以执行(PROT_EXEC),最后再通过函数调用执行相关代码。代码示例如下:

#include<stdio.h>
#include<memory.h>
#include<sys/mman.h>
typedef int (* add_func)(int a, int b);
int main() {
    char code[] = {
        0x00,0x00,0x01,0x0b, //0x0b010000, 等价于指令 add     w0, w0, w1
        0xc0,0x03,0x5f,0xd6 //0xd65f03c0, 等价于指令 ret
    }; //参考objdump对add函数的反汇编代码
    void * code_cache = mmap(NULL, sizeof(code), PROT_WRITE | PROT_EXEC,
                             MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    memcpy(code_cache, code, sizeof(code));
    add_func p_add = (add_func)code_cache;
    printf(“%d\n”, p_add(4,5));
    return 0;
}

示例中通过一个函数调用完成汇编代码的执行。实际上除了使用函数调用以外,还可以直接通过jmp完成相关的调用(函数调用的本质是通过call指令完成控制流的转移)。JVM执行编译后的代码原理和示例介绍基本类似,通过识别热点代码(例如Java中的add函数),并对热点代码进行编译优化,产生目标机器代码(类似于此处C代码中add函数的反汇编代码),然后执行目标机器代码。

在add函数的编译过程中直接使用了O2的编译优化级别,gcc默认的编译优化级别为O0。下面是使用默认编译优化级别产生的目标文件反汇编的结果。

0000000000400624 <add>:
    400624:       d10043ff        sub     sp,  sp, #0x10
    400628:       b9000fe0        str     w0, [sp, #12]
    40062c:       b9000be1        str     w1, [sp, #8]
    400630:       b9400fe1        ldr     w1, [sp, #12]
    400634:       b9400be0        ldr     w0, [sp, #8]
    400638:       0b000020        add     w0,  w1, w0
    40063c:       910043ff        add     sp,  sp, #0x10
    400640:       d65f03c0        ret

比较O2和O0的编译优化结果可以发现,O2的代码质量远高于O0的代码质量(指令明显少了很多)。那么O2采用的编译优化会更加复杂,编译耗时也更多。JVM中C1和C2编译器的目的也是生成不同指令的编译代码,可以简单理解为gcc不同编译级别产生的代码。当然JVM中C1和C2采用了不同的技术,使用的IR和编译优化手段都不相同。