首页 » 排名链接 » 深入解析java虚拟机:编译器到底为何物?面试大厂该怎么答?(编译编译器代码生成虚拟机)

深入解析java虚拟机:编译器到底为何物?面试大厂该怎么答?(编译编译器代码生成虚拟机)

少女玫瑰心 2024-11-02 23:49:44 0

扫一扫用手机浏览

文章目录 [+]

JIT指在程序启动后、执行前进行编译。
所以程序每次执行时都要进行一次或多次JIT编译。
JIT可以充分使用运行时收集到的数据,如receiver的类型、if分支计数等,然后进行PGO优化(Profiling-guidedOptimization)使程序运行性能达到峰值。
但是由于JIT的编译发生在程序执行过程中,需要运行时的内存、CPU资源,更重要的是JIT的编译时间也会影响程序执行时间,所以在设计JIT编译器时不能只考虑被编译程序的执行效率,编译效率(或称为JIT吞吐量)也是重要的考量标准,甚至影响整个编译器的设计架构。

AOT又叫静态编译,是指在运行前编译源代码,无须运行时开销,同时可以应用很多重量级的耗时优化,使编译后的机器代码能够快速启动,占用内存较小。
但是AOT缺少程序运行时的信息,对某些程序的峰值性能优化有限。

综合上述内容可知,JIT和AOT各有千秋,在选择编译方法时需要综合考虑语言特性、类型系统等,在一些情况下,还可以使用两者的组合。

深入解析java虚拟机:编译器到底为何物?面试大厂该怎么答?(编译编译器代码生成虚拟机) 排名链接
(图片来自网络侵删)
运行时代码生成

在讨论即时编译器前,首先要清楚一个重要问题:如何即时编译?要实现即时编译,需要一种动态生成可执行代码。
冯·诺依曼架构将数据和指令都储存在存储器中,这种架构可以将可执行指令视作数据写入内存,然后将那片内存的数据视作指令供CPU执行,简单的示例如代码清单7-1所示:

代码清单7-1 动态代码生成技术

#include <cstdio>#include <sys/mman.h>#include <cstring>// macOS + x64 + clangint main(){// 机器代码constexpr unsigned char code[]={0x55,0x48,0x89,0xe5,0x89,0x7d,0xfc,0x89,0x75,0xf8,0x8b,0x75,0xfc,0x03,0x75,0xf8,0x89,0xf0,0x5d,0xc3,};constexpr int ncode = sizeof(code)/sizeof(code[0]);// 分配内存,设置为可执行权限void mem = mmap(0, ncode, PROT_WRITE | PROT_EXEC,MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);memcpy(mem,code,ncode);// 将分配的内存地址转换为函数auto add_fun= (int()(int,int))mem;// 调用加法函数printf("%d",add_fun(3,2));munmap(mem,ncode);return 0;}

代码清单7-1展示了运行时代码生成的原理:先分配一片内存,然后将其设置为可执行,接着向内存中写入机器代码,最后将内存地址强制类型转换为函数指针再调用它。
代码清单7-1中的code就是需要运行时生成的机器代码,它对应加法函数,如代码清单7-2所示:

代码清单7-2 code的原始面貌

55 pushq %rbp ; 函数序幕48 89 e5 movq %rsp, %rbp89 7d fc movl %edi, -4(%rbp); 获取第一个参数89 75 f8 movl %esi, -8(%rbp); 获取第二个参数8b 75 fc movl -4(%rbp), %esi03 75 f8 addl -8(%rbp), %esi; 两个参数相加89 f0 movl %esi, %eax ; 结果放入eax5d popq %rbp ; 函数收尾c3 retq

如果直接手写二进制代码,显得太过“硬核”,同时代码也会完全不可维护、不可修改,所以在HotSpot VM中,生成机器代码依赖于宏汇编器MacroAssembler,它使用的是一种类似汇编的风格,如代码清单7-3所示:

代码清单7-3 HotSpot VM中的运行时代码生成

__ mov(rbp); // 生成55__ mov(rsp,rbp); // 生成48 89 e5__ mov(edi,Address(rbp,-4)); // 生成89 7d fc...

无须硬核手写机器代码,只需要写出汇编形式,宏汇编器就可以为它生成对应的机器代码。
除了即时编译器外,第5章的解释器生成也涉及动态代码生成技术,只是它是在虚拟机创建时初始化解释器的各个例程。
动态代码生成的另一个常见场景是编写shellcode。

JIT编译器

高性能从来都是虚拟机绕不开的话题,为此,JVM在性能方面做了很多努力。
早期虚拟机只有字节码解释器,后面实现了模板解释器,现在是模板解释器和即时编译器混合。
HotSpot VM包含两个即时编译器:客户端即时编译器(C1)和服务端即时编译器(C2)。

C1面向客户端程序,需要快速响应用户请求,它编译速度快,占用资源少,产出代码性能适中。
C2面向长期运行的服务端程序,允许虚拟机在编译上花更多时间以换取峰值运行性能。
它使用了更多激进的优化以提高性能,包括基于类层次分析的内联、快速路径慢速路径区分、全局值编号、常量传播、指令选择、图着色寄存器分配和窥孔优化等。
这些优化使得C2编译时间更长,占用资源更多,但产出代码性能极佳。

AOT编译器

即时编译本身是很快的,但是如果Java程序比较大,可能会花费更多时间在代码预热上,因为被即时编译的前提条件是方法的执行足够频繁。
为了了解方法执行频率,模板解释器会进行方法调用计数和回边计数,这就会占用部分内存空间,对于一些不常使用的Java方法来说是不必要的,如果能提前编译掉这些方法,就可以省去运行时性能计数开销,所以,AOT编译器应运而生。

Java 9包含了仅Linux可用的一个实验性质的AOT编译器jaotc[1],Java 11后的jaotc支持所有操作系统。
jaotc使用Graal编译器作为后端,它可以在虚拟机启动前将Java类编译成ELF格式的共享库,然后在虚拟机启动后加载共享库。
虚拟机将共享库看作Code Cache的补充数据,当加载Java类时,虚拟机查找共享库看能否找到已经存在的方法,如果找到就将它们关联起来。
jaotc编译产出的共享库的代码和普通JIT编译后的代码一样,加载到虚拟机后可能发生退优化、类卸载等行为。
对于一些长期运行的服务端程序,它们可能经历和JIT编译器相同的生命周期。

除此之外,目前jaotc的限制较多,能编译的Java代码和使用场景也比较有限,一个更好的选择是Graal VM平台的Substrate VM。

JVMCI JIT编译器

HotSpot VM使用C++语言,所以C2也是C++写成的。
使用C++没什么本质上的错误,但却有一些麻烦。
C++是一门不安全的语言,这意味着C++的错误可以造成虚拟机崩溃,同时由于代码年代久远,用C++写的C2变得很难维护,很难扩展。

编译器组件和垃圾回收器等组件不同,它无须一些低级语言特性,本质是将一个byte[]转换为另一个byte[]。
也许是Java比C++更安全,也许是探寻编译的本质,JEP 243提案通过了基于Java语言的JVM编译器接口JVMCI。
通过JVMCI接口可以使用Java语言编写即时编译器,然后“外挂式”地植入虚拟机来代替C2编译器。

JVMCI只是一个接口,它需要一个具体的实现者。
HotSpot VM自带的JVMCI实现和jaotc一样也要用到Graal编译器,需要附加虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -XX:+EnableJVMCI开启。

本文给大家讲解的内容是深入解析java虚拟机:编译概述,编译器下篇文章给大家讲解的是深入解析java虚拟机:编译概述,即时编译技术;觉得文章不错的朋友可以转发此文关注小编;感谢大家的支持!

相关文章

轻松get环保小技巧(环保开发用户软件生活)

环保,如今越来越贴近大众的日常生活。在发展快速的互联网时代,许多软件、系统的开发都给人们的生活带来了极大的方便,在环保这个领域也不...

排名链接 2025-02-09 阅读656 评论0