我本月开始写一篇文章,剖析写得不好的微基准。 毕竟,我们程序员痴迷于性能,并且我们每个人都希望了解我们编写,使用或批评的代码的性能特征。 当我偶尔写关于性能的文章时,我经常收到人们的电子邮件,说:“我写的这个程序表明,与上一篇文章相反,动态雾化比静态雾化更快!” 伴随此类电子邮件的许多所谓的“基准”程序或它们的运行方式,显示出对JVM如何实际执行Java字节码的认识非常缺乏。 因此,在写我打算写的文章之前(将在以后的专栏中介绍),让我们看一下JVM的内幕。 了解动态编译和优化是了解如何从坏的基准中分辨出好的微基准(关键是很少的基准)的关键。
动态编译-简要历史
Java应用程序的编译过程与C或C ++之类的静态编译语言不同。 静态编译器将源代码直接转换为可以在目标平台上直接执行的机器代码,并且不同的硬件平台需要不同的编译器。 Java编译器将Java源代码转换为可移植的JVM字节码,这是JVM的“虚拟机指令”。 与静态编译器不同,javac很少进行优化-由编译器以静态编译语言完成的优化是在程序执行时由运行时执行的。
第一代JVM被完全解释。 JVM解释了字节码,而不是将其编译为机器代码并直接执行机器代码。 当然,这种方法不能提供最佳的性能,因为系统执行解释器所花的时间比应该运行的程序要多。
即时编译
对于概念验证的实现来说,解释是可以的,但是早期的JVM由于速度慢而很快受到了不好的好评。 下一代JVM使用即时(JIT)编译器来加快执行速度。 严格定义,基于JIT的虚拟机会在执行之前将所有字节码转换为机器代码,但是这样做是偷懒的:JIT仅在知道要执行的代码路径时才编译代码路径(因此,名称,名称, 即时编译)。 这种方法使程序可以更快地启动,因为在开始执行之前不需要冗长的编译阶段。
JIT方法似乎很有希望,但是它有一些缺点。 JIT编译消除了解释的开销(以一些额外的启动成本为代价),但是由于一些原因,代码优化的水平中等。 为了避免Java应用程序的大量启动损失,JIT编译器必须快速运行,这意味着它不能花太多时间进行优化。 早期的JIT编译器在进行内联假设时比较保守,因为他们不知道以后会加载哪些类。
从技术上讲,基于JIT的虚拟机会在执行每个字节码之前先对其进行编译,但术语JIT通常用于指代将字节码动态编译为机器代码的方法,甚至也包括那些能够解释字节码的代码。
热点动态编译
HotSpot执行过程结合了解释,概要分析和动态编译。 HotSpot不会在执行之前将所有字节码转换为机器代码,而是首先以解释器的身份运行,并且仅编译“热”代码-最常执行的代码。 在执行时,它会收集概要分析数据,用于确定执行频率最高的代码段,足以值得编译。 仅编译频繁执行的代码具有几个性能优势:不会浪费时间去编译很少执行的代码,因此,编译器可以花更多的时间在热代码路径的优化上,因为它知道这样的时间将被很好地利用。 此外,通过推迟编译,编译器可以访问分析数据,该数据可用于改进优化决策,例如是否内联特定的方法调用。
为了使事情变得更复杂,HotSpot带有两个编译器:客户端编译器和服务器编译器。 默认是使用客户端编译器; 您可以在启动JVM时通过指定-server
开关来选择服务器编译器。 服务器编译器已经过优化,可以最大程度地提高峰值运行速度,并且适用于长时间运行的服务器应用程序。 已对客户端编译器进行了优化,以减少应用程序启动时间和内存占用量,与服务器编译器相比,采用的复杂优化更少,因此,所需的编译时间也更少。
HotSpot服务器编译器可以执行各种令人印象深刻的优化。 它可以执行静态编译器中的许多标准优化,例如代码提升,通用子表达式消除,循环展开,范围检查消除,死代码消除和数据流分析,以及各种其他非优化方法。在静态编译语言中实用,例如积极地内联虚拟方法调用。
连续重新编译
HotSpot方法的另一个有趣的方面是,编译不是一个全有或全无的主张。 在解释了一定数量的代码路径后,将其编译为机器代码。 但是JVM会继续进行性能分析,并且如果JVM确定代码路径特别热,或者将来的性能分析数据表明有进一步优化的机会,则可以稍后以更高的优化级别再次重新编译代码。 JVM可以在单个应用程序执行中多次重新编译相同的字节码。 要了解编译器在做什么,请尝试使用-XX:+PrintCompilation
标志调用JVM,这会导致编译器(客户端或服务器)在每次运行时打印一条短消息。
堆叠更换
HotSpot的初始版本一次执行一种编译方法。 如果某个方法累积执行的循环迭代次数超过一定次数(HotSpot的第一个版本为10,000),则该方法被认为是热方法,该迭代是通过将计数器与每种方法相关联并在每次执行向后转移时递增该计数器来确定的。 但是,在编译方法之后,直到方法退出并重新输入后,它才切换到编译版本-编译版本仅用于后续调用。 在某些情况下,结果是从未使用过编译版本,例如ÿ