javac关闭编译错误检查 java关闭编译告警参数_javac关闭编译错误检查

javac关闭编译错误检查 java关闭编译告警参数_javac参数 编译警告关闭_02

当今Java语言被广为接受的优点之一就有即时编译,即时编译的存在使得Java应用可以运行时间的增长而获得更高的性能。 如果有对jvm做过研究的朋友,一定听说过这样一段话:由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,Java对象实例都分配在堆上也变得不那么绝对了。虚拟机发展到今天,即时编译也是我们对jvm讨论的话题之一,今天我也来对即时编译进行一个详述。



JIT(just-in-time)

Coding的哔哔叨叨

JIT即just-in-time的缩写,即时编译技术,可以加快java的执行速度。我们都知道,从源代码,经javac编译生成.class文件字节码,在经过类加载器,加载到内存,jvm通过解释字节码翻译成对应的机器指令,逐条读入,逐条翻译解释,这就是 jvm解释执行的过程。


很显然,解释执行,其执行速度必然会比可执行的二进制字节码程序慢很多,为了提高执行速度,引入了即时编译技术。

Java程序最初是通过解释器(interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认为是“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成与本地机器相关的机器码,并进行各种层次的优化,完成这个任务的编译器称之为“即时编译器(just-in-time compiler)”。

javac关闭编译错误检查 java关闭编译告警参数_javac参数 编译警告关闭_02

注意:即时编译器并不是java虚拟机的必须部分,java虚拟机规范并没有规定必须要有即时编译器的存在,并且,更没有要求或指导如何实现即时编译器;但是即时编译器的好坏、代码优化程度高低却是衡量一款商用虚拟机的重要指标之一,它也是虚拟机最核心且最能体现虚拟机技术水平的的部分。

由于上述原因,所以即时编译器完全是某虚拟机具体实现的相关内容,所以,如无特殊说明,本文所提到的即时编译器都是Hotspot虚拟机内的即时编译器;所提到的虚拟机也是专门特指Hotspot虚拟机。

JVM运行原理

Coding的哔哔叨叨

这儿我们贴出一张图,大家结合着图进行理解即可。

javac关闭编译错误检查 java关闭编译告警参数_编译器_04

javac关闭编译错误检查 java关闭编译告警参数_javac参数 编译警告关闭_05

为什么Hotspot要使用解释器与编译器并存的架构?

Coding的哔哔叨叨

首先要说明,并不是所有的虚拟机实现都采用解释器与编译器并行的方案,但许多的主流虚拟机,如Hotspot,都同时包含解释器和编译器。解释器与编译器各有优势: 当程序需要快速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行;当程序运行环境内存资源限制较大(如嵌入式系统),可以使用解释器节约内存。反之可以使用编译器提高执行效率,此外,编译后如果出现“罕见陷阱”,可以逆优化退回到解释执行。

javac关闭编译错误检查 java关闭编译告警参数_javac关闭编译错误检查_06

编译的时间开销

Coding的哔哔叨叨

我们先抽象看下解释器和编译器的执行过程。

  • 解释器:输入代码->[解释器解释执行]->执行结果
  • 编译器:输入代码->[编译器编译]->编译后的代码->执行结果。

从上面的抽象执行过程来看,JIT编译执行,要比解释执行慢,多了一个执行编译后代码的过程,所以针对“只执行一次的代码”,解释执行总要比编译执行快。而我们通常所说的,JIT编译执行,比解释执行快,并不是说“编译”这个动作比“解释”这个动作快,而是在非“只执行一次的代码”环境下,JIT编译执行要比解释执行快,要注意这个前提环境,而我们所指的不止执行一次的代码,有以下两个场景:

  1. 方法被多次调用,相反类似类构造器(class initializer,())只被调用一次。
  2. 存在循环。

只有频繁执行的代码,才会保证JIT编译执行的正向收益。

编译的空间开销

Coding的哔哔叨叨

对java语言来说,编译后的代码相对于class字节码,膨胀达10倍是很正常的,同上面所说的时间开销一样,只有针对频繁执行的代码,才能保证有正向收益,才值得去编译,如果把所有的代码都进行编译,会很明显的增加代码所占空间。

JIT编译,会将编译后的汇编指令保存在代码缓存中,代码缓存是固定大小的,当jvm代码缓存一旦被填满,jvm就不能编译更多的代码。

所以,这也解释了,为什么虚拟机不完全采用JIT编译执行,而是选择解释器与编译器混合共存的执行引擎。

现在并没有一个好的机制可以确定一个特定的应用到底需要多大的代码缓存。因此,当需要提高代码缓存时,这将是一种凑巧的操作,一个通常的做法是将代码缓存变成默认大小的两倍或四倍。

可以通过 –XX:ReservedCodeCacheSize=Nflag(N 就是之前提到的默认大小)来最大化代码缓存大小。代码缓存的管理类似于 JVM 中的内存管理:有一个初始大小(用-XX:InitialCodeCacheSize=N 来声明)。代码缓存的大小从初始大小开始,随着缓存被填满而逐渐扩大。代码缓存的初始大小是基于芯片架构(例如 Intel 系列机器,client 编译器模式下代码缓存大小起始于 160KB,server 编译器模式下代码缓存大小则起始于 2496KB)以及使用的编译器的。重定义代码缓存的大小并不会真正影响性能,所以设置 ReservedCodeCacheSize 的大小一般是必要的。

再者,如果 JVM 是 32 位的,那么运行过程大小不能超过 4GB。这包括了 Java 堆,JVM 自身所有的代码空间(包括其本身的库和线程栈),应用程序分配的任何的本地内存,当然还有代码缓存。

所以说代码缓存并不是无限的,很多时候需要为大型应用程序来调优(或者甚至是使用分层编译的中型应用程序)。比如 64 位机器,为代码缓存设置一个很大的值并不会对应用程序本身造成影响,应用程序并不会内存溢出,这些额外的内存预定一般都是被操作系统所接受的。

Hotspot的两个不同编译器

Coding的哔哔叨叨

Hotspot内置了两个解释编译器: client compiler(c1)和server compiler(c2),分别用在客户端和服务端。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。c1编译器有更快的编译速度,c2编译器有更高的编译质量。

哪些代码会被即时编译?

Coding的哔哔叨叨

哪些代码会被称为热点代码,其实我们在上面也提过过,以下两种场景:

  1. 被多次调用的方法。
  2. 被多次执行的循环体。

javac关闭编译错误检查 java关闭编译告警参数_javac参数 编译警告关闭_02

注意:上面两种情况,编译器都是以整个方法作为编译对象,编译方法发生在方法执行过程中,我们称之为:栈上替换(OSR,On Stack Replacement),方法栈针还在栈上,方法就被替换了。

如何编译为本地代码?

Coding的哔哔叨叨

Server Compiler和Client Compiler两个编译器的编译过程是不一样的。 对Client Compiler来说,它是一个简单快速的编译器,主要关注点在于局部优化,而放弃许多耗时较长的全局优化手段。 而Server Compiler则是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个充分优化过的高级编译器。


HotSpot的热点探测技术

Coding的哔哔叨叨

目前主流的热点探测技术有两种:

  • 基于采样的热点探测

从字面也能基本理解,这种方法基本是周期性的检查各个线程的栈顶,如果发现某个方法经常性的出现在栈顶,那么这个方法就是“热点代码”。这种方式简单有效,但是也存在很明显的问题,很难精准的确认一个方法是热点方法,容易因为线程阻塞或者其他的一些外界因素的影响而扰乱热点探测。

  • 基于计数器的热点探测

采用这种方式的虚拟机会为每个方法(甚至是代码)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值,就认为是“热点方法”。这种方式实现起来就复杂了,需要为每个方法建立并维护计数器,而且不能直接获取方法的调用关系,但是这种方式的统计结果就相对精确严谨。 Hotspot虚拟机采用的是计数器热点探测方式。 ,为每个方法都维护了两个计数器:方法调用计数器和回边计数器。在确定虚拟机参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

方法调用计数器

顾名思义,这个计数器用于统计方法被调用的次数。当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。如果不做任何设置,执行引擎并不会同步等待编译请求完成,而是继续进行解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个方法的调用入口地址就会系统自动改写成新的,下一次调用该方法时就会使用已编译的版本。

javac关闭编译错误检查 java关闭编译告警参数_编译器_08

回边计数器

它的作用就是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”。


javac关闭编译错误检查 java关闭编译告警参数_javac关闭编译错误检查_09

JIT的知识点,大部分应该都提到了,还有啥没提到的,留言,补充。

文章部分图片来源于网络其他优秀工程师的分享。


最后还想给大家补充个知识点,关于java的javac编译和即时编译。在刚开始接触到java的时候,每次谈到编译,总是搞不清到底说的是那种编译,在这也简单聊一下,给大家做个简单的区分。

javac编译与JIT编译

Coding的哔哔叨叨

我们通常所说的编译,有两种情形,一种是从源代码(.java)编译为字节码(.class);一种是虚拟机执行字节码的过程,从字节码编译为本地机器语言。所以在提到编译的时候,一定要区分清是哪种(刚开始结束java的时候被这个搞得晕乎的很久)。我们一般把javac的编译过程称之为:前端编译;把JIT编译称之为后端编译。如果可能,下篇文章,我把java的前端编译过程进行一下说明。

javac关闭编译错误检查 java关闭编译告警参数_编译器_10

不积跬步,无以至千里。