JVM的即时编译器JIT之简单介绍
注:本文源于《深入JAVA虚拟机》一书,提取部分骨干内容,算是读书笔记吧。
本文写于2011年11月
问题:
- 为何JVM需要使用解释器和编译器并存的架构?
- JVM为什么要实现两个不同的即时编译器?
- 程序何时会使用解释器执行?何时会使用编译器执行?
- 哪些程序代码会被编译成为本地代码?如何编译?
- JAVA代码的执行效率就一定比C,C++静态执行的执行差?JAVA代码解析执行有何优势?
序:
从计算机程序出现的第一天起,对效率的追逐就是程序天生的坚定的信仰,这个过程犹如一场没有终点,永不停歇的F1方程式竞赛。
程序员犹如车手,技术平台则是赛道上飞驰的赛车。
JVM即时编译器:即时编译器(Just In Time Compiler) 简称JIT
JAVA程序最初是通过解释器(Interpreter)进行解释执行的,当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。
为了提高热点代码的执行效率,就会将这些“热点代码”编译成与本地机器相关的机器码,进行各个层次的优化。 完成这个任务的编译器就是即时编译器(JIT)。
即时编译器的性能好坏,代码的优化程度高低是衡量一款商用虚拟机优秀与否的最关键指标之一,它是虚拟机最核心最能体现技术水平的部分。
JVM的两款即时编译器JIT
JVM中默认内置了两款即时编译器,称为Client Compiler和Server Compiler。
可以用指定参数的方式,指定采用Client模式和Server模式。默认是mixed模式。
java –Xint 解析 java –Xcomp 编译
为什么解析器与编译器并存:
1、当程序需要迅速启动和执行的时候,解析器首先发挥作用,省去编译的时间,立即执行。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
2、当机器内存限制比较大,可以用解析方式节约内存,反之可以用编译提升效率。
3、解析器还可以作为编译器的“逃生门”。当例如加载了新类后类型结构发生变化,可以采用逆优化,退回到解析状态继续执行。
Client Compiler和Server Compiler
Client Compiler和Server Compiler会实现分层编译(JDK 1.7默认有)。
第0层 程序解析执行,解析器不开启性能监控,可触发第一层编译。
第1层 编译成本地相关代码,进行简单优化
第2层 除编译成本地相关代码外,还进行成编译耗时较长的优化。
Client Compiler获得更高的编译速度 Server Compiler获得更好的编译质量,无须承担性能监控的任务
热点代码
两类:1、多次被调用的方法
2、多次被执行的循环体
热点判定方式,两种:
1、基于采样的方式探测(Sample Based Hot Spot Detection) 。周期性检测各个线程的栈顶,发现某个方法经常出险在栈顶,就认为是热点方法。好处就是简单,缺点就是无法精确确认一个方法的热度。容易受线程阻塞或别的原因干扰热点探测。
2、基于计数器的热点探测(Counter Based Hot Spot Detection)。某个方法超过阀值就认为是热点方法,触发JIT编译。(涉及计数器的热度半衰减过程)
两个计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)
触发了JIT编译,编译工作完成,这个方法的入口就会被系统自动改为新的编译入口,就会调用编译的版本。
JVM编译优化技术-----逃逸分析
JDK设计团队,几乎把所有的优化措施都集中在即时编译器中。一般认为,编译器的本地会比javac的产生的字节码更优秀。
常见的优化技术很多:例如公共表达式的消除,数组边界检查的消除,方法的内联(最重要的优化技术)
介绍一个JVM最前沿的优化技术:逃逸分析:
逃逸分析:当一个对象被定义后,可能被外部方法引用,例如被当作参数传递到其他方法中,称为方法逃逸。可以被其他线程访问,这个称为线程逃逸。
若能证明这个对象不会逃逸到其他方法或线程中,就可以进行高效的优化。
1、栈上分配 在堆上分配对象内存,回收整理内存需要消耗时间,若在栈上分配内存将是个不错的主意。被对象占用的空间就可以随帧栈的就出栈而销毁。大量的对象随方法的结束而自动销毁,GC也减轻压力。
2、同步消除 若不会线程逃逸,不会有竞争,方法上的同步措施就会消除。
3、标量替换 JAVA的原始类型无法再分就是一个标量,对象就是聚合量。对象若可以被拆分成标量,直接在栈上分配,就是类似栈上分配内存,甚至分配到高速缓存中。
逃逸分析不成熟的原因:不能保证逃逸分析的性能收益大于它的消耗。
Java与C/C++编译器对比
Java与C/C++编译器的对比,代表了最经典的即时编译器与静态编译器的对比。除了自身API的实现的好坏,更多是一场“拼编译器”和“拼输出代码质量”的游戏。
看看这两种语言的编译器的优劣:
1、即时编译器运行占用的是用户的运行时间,具有很大的时间压力,它提供的优化手段严重受制与编译的成本。而编译的时间在静态编译中并不是主要关注点。
2、java语言是动态的类型安全语言,意味着由虚拟机确保程序不会违反语言的语义或访问非结构化内存。虚拟机需要频繁的动态检查,如空指针,数组越界,继承关系等,总体消耗不少时间。
3、java语言使用虚方法的频率远大于C/C++,意味对方法的接收者进行多态的选择频率远大于C/C++,意味着即时编译器的优化难度远远大于C/C++编译器。
4、java语言是动态扩展的语言,运行时会加载新的类,改变程序的继承关系,使得很多全局优化难以进行。只能采用激进的方式,在运行时撤销或重新进行一些优化。
5、java语言的对象是在堆上分配,只有方法的局部变量才在栈上分配。而C/C++语言有多种分配方式,既可以在堆上分配,又可以在栈上分配,减轻了内存回收的压力。另外C/C++语言主要由用户代码回收内存,不存在无用对象筛选的过程,效率要比垃圾回收机制要高。
但是:
Java语言在性能上的劣势都是为了换取开发效率上的优势而付出的代价。动态安全,动态扩展,垃圾回收这些“拖后腿”的特性都是为JAVA的开发效率做出了很大的贡献。
何况java即时编译器能做的,C/C++的静态优化编译器不一定能够做:
由于C/C++的静态编译,以运行性能监控为基础的优化措施它都无法进行,如调用频率预测,分支频率预测,裁剪未使用分支等,这些都是称为java语言独有的性能优势。