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

简介

当 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 -version

java 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 -version

java 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 -version

java 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 将不执行内联操作。而方法体的大小阈值,我们也可以通过参数设置来优化:

经常执行的方法,默认情况下,方法体大小小于 325 字节的都会进行内联,我们可以通过-XX:MaxFreqInlineSize=N来设置大小值;

不是经常执行的方法,默认情况下,方法大小小于 35 字节才会进行内联,我们也可以通过-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 编译会对这个对象的方法锁进行锁消除。例如:

public static String getString(String s1, String s2) {

StringBuffer sb = new StringBuffer();

sb.append(s1);

sb.append(s2);

return sb.toString();

}

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

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

-XX:-EliminateLocks 关闭锁消除

标量替换

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

例如:

public void foo() {

TestInfo info = new TestInfo();

info.id = 1;

info.count = 99;

// to do something

}

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

public void foo() {

id = 1;

count = 99;

// to do something

}

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

-XX:+EliminateAllocations 开启标量替换(jdk1.8 默认开启)

-XX:-EliminateAllocations 关闭就可以了

总结

今天的内容,由最基本的常识方法内部行数和逻辑需要尽可能简单引出,了解了 JVM 通过即时编译对热点代码进行优化的过程。如果你有什么想法,欢迎在下方留言。

有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。

本文由博客一文多发平台 OpenWrite 发布!

java 及时编译_Java 面试-即时编译( JIT )相关推荐

  1. java程序编译_Java程序的编译过程

    Java的编译期是一个模糊的概念,需要具体分析. 将 *.java文件转为 *.class的过程称为编译器的前端(前端编译).例如:JDK的javac编译器. 把字节码( *.class文件) 转变为 ...

  2. java list 初始化_Java面试大全(十)

    第五章 Java 的高级 1.说说你对 Java 中反射的理解 Java 中的反射首先是能够获取到Java 中要反射类的字节码,获取字节码有三种方法:1. Class.forName(classNam ...

  3. java无法编译_Java静态方法无法编译

    当我编译这段代码时,会出现以下错误. 中的提取字符(java.郎,字符串,int) 问题2不能应用于(). 我应该修复什么? 谢谢. import java.util.Scanner; public ...

  4. java sql in 预编译_JAVA使用预编译防止SQL注入

    原标题:JAVA使用预编译防止SQL注入 这是一条正常的SQL语句: SELECT * FROM users WHERE name ='$var'; 但是当变量var为 ' or true or '时 ...

  5. java基本特性_Java面试总结之Java基础

    无论是工作多年的高级开发人员还是刚入职场的新人,在换工作面试的过程中,Java基础是必不可少的面试题之一.能不能顺利通过面试,拿到自己理想的offer,在准备面试的过程中,Java基础也是很关键的.对 ...

  6. java自我介绍_JAVA面试技巧之自我介绍

    [如何进行自我介绍] 自我介绍这个问题,不用多说了,面试必定会问!如果想要在自我介绍的时候就能够打动面试官,吸引面试官对我们的兴趣,那么像我们这种接受过Java培训的程序员的自我介绍当然不能和应届生或 ...

  7. java笔试题_Java面试才到笔试就没有然后了?快来签收,高频笔试57题及解答

    前言 很多人面试之前,可能没有在互联网公司工作过或者说工作过但年头较短,不知道互联网公司技术面试都会问哪些问题? 再加上可能自己准备也不充分,去面试没几个回合就被面试官几个问题打蒙了,甚至笔试都过不了 ...

  8. java异或_JAVA面试必备之HashMap必会点

    今天我们就面试会问到关于HashMap的问题进行一个汇总,以及对这些问题进行解答. 1.HashMap的数据结构是什么? 2.为啥是线程不安全的? 3.Hash算法是怎样实现的? 4.HashMap是 ...

  9. java 取余_JAVA面试解析(有赞)

    一面引言 说在前面的话: 本文适合人群:急等着换工作的人 我承认刷面试题很有用的,纵观几年来的JAVA面试题,你会发现每家都差不多.比如,你仔细观察,你会发现,HashMap的出现几率未免也太高了吧! ...

最新文章

  1. 阿里P9大佬总结必备的算法和工具,被10万算法工程师点赞
  2. 使用trash-cli防止rm -rf 误删除带来的灾难(“事前”非“事后”)
  3. flask简单的登录demo
  4. iOS 获取app进程被杀死事件applicationWillTerminate
  5. PyQt5 技术篇-通过参数控制Dialog窗口增加?问号按钮
  6. springboot 定时器_基于SpringCloud?+?SpringBoot的 SaaS型微服务脚手架源码分享
  7. 计算机硬盘登记表,硬盘固件的 P 表与 G 表
  8. Batch Normalization深入理解
  9. Java计算多线程运行时间的简单方式
  10. Python 中类的继承:属性初始化、类型判断、多态、多继承和对象信息的获取
  11. linux下的定时任务和延迟任务
  12. 63.1拓展之box-shadow属性
  13. 微机原理是微型计算机与接口技术吗,《微机原理与接口技术》课程教学大纲
  14. 浏览器默认首页被360导航篡改解决办法
  15. js实现连续英文字符自动换行
  16. 企业打造营销型网站的7条黄金法则
  17. access查询两列信息合并输出_如何在Access中合并两个数据表中的数据
  18. 点评阿里云盛大云代表的云计算IaaS产业---中国云计算
  19. 中标麒麟安装node、nginx
  20. 备考通信复试过程中的一些知识点总结梳理——信道编码

热门文章

  1. C/C++中关于qsort的使用
  2. 吴裕雄 python 机器学习——多项式贝叶斯分类器MultinomialNB模型
  3. 数据库学习--wildfly配置postgreSQL数据源
  4. Java经典设计模式:五大创建型模式
  5. STM32应用笔记转载
  6. (并查集 建立关系)Parity game -- POJ -1733
  7. MVC中的View2(转)
  8. P2P之UDP穿透NAT的原理与实现
  9. Sharding-JDBC水平分表(分片策略)_Sharding-Sphere,Sharding-JDBC分布式_分库分表工作笔记008
  10. MQTT工作笔记0003---产品和设备