Java 面试——即时编译( JIT )

当我们在写代码时,一个方法内部的行数自然是越少越好,这样逻辑清晰、方便阅读,其实好处远不止如此,通过即时编译,甚至可以提高执行时的性能,今天就让我们好好来了解一下其中的原理。

简介

当 JVM 的初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是 即时编译 。

最初,JVM 中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为 热点代码 。

为了提高热点代码的执行效率,在运行时,即时编译器(JIT,Just In Time)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中。

分类

在 HotSpot 虚拟机中,内置了两种 JIT,分别为 C1 编译器 和 C2 编译器 ,这两个编译器的编译过程是不一样的。

C1 编译器

C1 编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序,也称为 Client Compiler ,例如,GUI 应用对界面启动速度就有一定要求。

C2 编译器

C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序,也称为 Server Compiler ,例如,服务器上长期运行的 Java 应用对稳定运行就有一定的要求。

分层编译

在 Java7 之前,需要根据程序的特性来选择对应的 JIT,虚拟机默认采用解释器和其中一个编译器配合工作。

Java7 引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,我们也可以通过参数 -client 或者 -server 强制指定虚拟机的即时编译模式。

分层编译将 JVM 的执行状态分为了 5 个层次:

第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译;

第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling;

第 2 层:也称为 C1 编译,开启 Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译;

第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译;

第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

对于 C1 的三种状态,按执行效率从高至低:第 1 层、第 2层、第 3层。

通常情况下,C2 的执行效率比 C1 高出30%以上。

在 Java8 中,默认开启分层编译, -client 和 -server 的设置已经是无效的了。如果只想开启 C2,可以关闭分层编译( -XX:-TieredCompilation ),如果只想用 C1,可以在打开分层编译的同时,使用参数: -XX:TieredStopAtLevel=1 。

你可以通过 java -version 命令行可以直接查看到当前系统使用的编译模式:

C:\Users\Administrator>java -versionjava version"1.8.0_45"Java(TM) SE Runtime Environment (build 1.8.0_45-b14)Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)

mixed mode 代表是默认的混合编译模式,除了这种模式外,我们还可以使用 -Xint 参数强制虚拟机运行于只有解释器的编译模式下,这时 JIT 完全不介入工作;也可以使用参数 -Xcomp 强制虚拟机运行于只有 JIT 的编译模式下。例如:

C:\Users\Administrator>java -Xint -versionjava version"1.8.0_45"Java(TM) SE Runtime Environment (build 1.8.0_45-b14)Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, interpreted mode)C:\Users\Administrator>java -Xcomp -versionjava version"1.8.0_45"Java(TM) SE Runtime Environment (build 1.8.0_45-b14)Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, compiled mode)

触发标准

在 HotSpot 虚拟机中, 热点探测 是 JIT 的触发标准。

热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法” 。

虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。

方法调用计数器

方法调用计数器用于统计方法被调用的次数,默认阈值在 C1 模式下是 1500 次,在 C2 模式在是 10000 次,可通过 -XX: CompileThreshold 来设定;而在分层编译的情况下 -XX: CompileThreshold 指定的阈值将失效,此时将会根据当前待编译的方法数以及编译线程数来动态调整。当方法计数器和回边计数器之和超过方法计数器阈值时,就会触发 JIT 编译器。

回边计数器

回边计数器用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值,在不开启分层编译的情况下,C1 默认为 13995,C2 默认为 10700,可通过 -XX: OnStackReplacePercentage=N 来设置;而在分层编译的情况下, -XX: OnStackReplacePercentage 指定的阈值同样会失效,此时将根据当前待编译的方法数以及编译线程数来动态调整。

建立回边计数器的主要目的是为了触发 OSR(On StackReplacement)编译,即栈上编译。在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直接将执行代码替换,执行缓存的机器语言。

优化技术

JIT 编译运用了一些经典的编译优化技术来实现代码的优化,即通过一些例行检查优化,可以智能地编译出运行时的最优性能代码。主要有两种: 方法内联 、 逃逸分析 。

方法内联

调用一个方法通常要经历压栈和出栈。调用方法是将程序执行顺序转移到存储该方法的内存地址,将方法的内容执行完后,再返回到执行该方法前的位置。

这种执行操作要求在执行前保护现场并记忆执行的地址,执行后要恢复现场,并按原来保存的地址继续执行。因此,方法调用会产生一定的时间和空间方面的开销(其实可以理解为一种 上下文切换 的精简版)。

那么对于那些方法体代码不是很大,又频繁调用的方法来说,这个时间和空间的消耗会很大。

方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。

JVM 会自动识别热点方法,并对它们使用方法内联进行优化。我们可以通过 -XX:CompileThreshold 来设置热点方法的阈值。但要强调一点,热点方法不一定会被 JVM 做内联优化,如果这个方法体太大了,JVM 将不执行内联操作。而方法体的大小阈值,我们也可以通过参数设置来优化:

-XX:MaxFreqInlineSize=N-XX:MaxInlineSize=N

之后我们就可以通过配置 JVM 参数来查看到方法被内联的情况:

// 在控制台打印编译过程信息-XX:+PrintCompilation// 解锁对 JVM 进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对 JVM 进行诊断-XX:+UnlockDiagnosticVMOptions// 将内联方法打印出来-XX:+PrintInlining

热点方法的优化可以有效提高系统性能,一般我们可以通过以下几种方式来提高方法内联:

通过设置 JVM 参数来减小热点阈值或增加方法体阈值,以便更多的方法可以进行内联,但这种方法意味着需要占用更多地内存;

在编程中,避免在一个方法中写大量代码,习惯使用小方法体;

尽量使用 final、private、static 关键字修饰方法,编码方法因为继承,会需要额外的类型检查。

此处就联系到了最开始提出的观点,一个方法中的内容越少,当该方法经常被执行时,则容易进行方法内联,从而优化性能。

逃逸分析

逃逸分析(Escape Analysis)是判断一个对象是否被外部方法引用或外部线程访问的分析技术,编译器会根据逃逸分析的结果对代码进行优化。

可以通过JVM参数进行设置:

-XX:+DoEscapeAnalysis 开启逃逸分析(jdk1.8默认开启)-XX:-DoEscapeAnalysis 关闭逃逸分析

其具体优化方法主要有三种: 栈上分配 、 锁消除 、 标量替换 。

栈上分配

在 Java 中默认创建一个对象是在堆中分配内存的,而当堆内存中的对象不再使用时,则需要通过垃圾回收机制回收,这个过程相对分配在栈中的对象的创建和销毁来说,更消耗时间和性能。

这个时候,逃逸分析如果发现一个对象只在方法中使用,就会将对象分配在栈上。

但是,HotSpot 虚拟机目前的实现导致栈上分配实现比较复杂,可以说,在 HotSpot 中暂时没有实现这项优化,所以大家可能暂时无法体会到这种优化(我看的资料显示在 Java8 中还没有实现,如果大家有什么其他的发现,欢迎留言)。

锁消除

如果是在单线程环境下,其实完全没有必要使用线程安全的容器,但就算使用了,因为不会有线程竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。例如:

publicstaticString getString(String s1,String s2) {StringBuffer sb= newStringBuffer();sb.append(s1);sb.append(s2);returnsb.toString();}

可以通过JVM参数进行设置:

-XX:+EliminateLocks 开启锁消除(jdk1.8默认开启)-XX:-EliminateLocks 关闭锁消除

标量替换

逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量替换。

例如:

publicvoidfoo(){ TestInfo info =newTestInfo(); info.id =1; info.count =99;// to do something}

逃逸分析后,代码会被优化为:

publicvoidfoo(){ id =1; count =99;// to do something}

可以通过JVM参数进行设置:

-XX:+EliminateAllocations 开启标量替换(jdk1.8默认开启)-XX:-EliminateAllocations 关闭就可以了

需要java学习路线图的私信笔者“java”领取哦!另外喜欢这篇文章的可以给笔者点个赞,关注一下,每天都会分享Java相关文章!还有不定时的福利赠送,包括整理的学习资料,面试题,源码等~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 1 编译简介 根据完成任务不同,可以将编译器的组成部分划分为前端(Front End)与后端(Back End)。...
    QuinnSun阅读 772评论 0 1
  • 部分的商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会...
    胡二囧阅读 811评论 0 1
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,528评论 3 83
  • java编译器,java解释器 1.java程序是一种可跨平台执行的语言,之所以可以跨平台,是因为jvm的存在,J...
    rabbit_coding阅读 6,988评论 2 18
  • 昨天和一朋友聊天,他问我一个问题:一个人一辈子最重要的能力是什么? 我想了很久,最终也没有回答他的问题,我不知道怎...
    黑金公主阅读 445评论 0 0