1、虚拟机运行模式
java是一种解释性编程语言,在Hotspot实现中,提供了解释器和即时编译器,即时编译器能将热点代码编译为效率更高的机器代码,以提升执行效率,加快系统运行速度。
hotspot运行模式配置:
- 解释模式:可通过 -Xint 选项指定,让 JVM 以解释模式运行 Java 程序。
- 编译模式:可通过 -Xcomp 选项指定,让 JVM 以编译模式运行 Java 程序。
- 混合模式:可通过 -Xmixed 选项指定,让 JVM 以解释+编译模式运行 Java 程序,这也是 Hotspot 的默认模式.
2、解释器
系统启动时,解释器按照预定义的规则,为所有字节码分别创建能够在具体计算机平台上运行的机器码,并存放在特定位置。当运行时环境需要解释字节码时,就到指定位置取出相应的code,直接在机器上运行。
3、即时编译
解释性语言的优点是可移植性强,可以放在任何有解释器的机器上运行,但其运行性能较差。原因是解释器需要实时将指令解释为机器码运行,而且解释器未对指令做更好的优化。
编译器有足够的上下文信息、统计信息等,可以对代码进行更好的优化。
3.1、编译器类型
编译器类型有client(C1编译器)和server(C2编译器)两种类型。两种编译器的主要差别是编译代码的时机不同。client编译器在代码执行的开始阶段,其编译速度比server编译器块,而编译的代码比server编译的多。server编译器在编译的时候会更好地进行优化。
一种列外是分层编译,在虚拟机启动时由client快速编译,随着代码变热后使用server编译器重新编译,以提高系统的整体性能。
编译器版本:
- -client:32位client编译器
- -server:32位server编译器
- -d64:64位server编译器
- -XX:+TieredCompilation:分层编译器,其使用server编译器。
3.2、即时编译器版本选择
- 32位系统:必须选用32位的JVM,即client编译器;
- 64位系统:可选32位或64位。如果内存小于3GB,32位编译器更快,占用内存更少。因为虚拟机指针只有32位,所占内存空间少,而且操作代价也少于64位的指针。32位编译器的缺点是内存最多只能为4GB。而且还包括堆、永久带、本地代码及JVM所使用的本地内存。另外,32位编译器无法使用64位CPU的寄存器,无法进行寄存器相关的优化,故大量使用long或double变量的应用会比较慢。在 32 位 JVM 上运行的程序、只要与 32 位寻址空间吻合,无论机器是 32 位还是 64 位,都要比在类似配置的 64 位 JVM 上运行时快 5 %到 20 %。
3.3、代码缓存
虚拟机编译时,会在代码缓存中保留编译之后的汇编语言指令集。代码缓存的大小固定,一旦填满,虚拟机就不能编译更多代码了。故若代码缓存过小,只有少部分热点代码被编译执行,其他的则没有,最终会导致大部分代码都是解释运行,系统性能较差。
以上情况在client或分层编译的情况下很常见。在使用常规server编译器时,因通常只有少量类会被编译,所以代码缓存不太可能被填满。而在client或分层编译时,可被编译的类可能会非常多。
参数 | 说明 |
---|---|
-XX:CodeCacheExpansionSize | 配置CodeCache空间扩展大小的参数 |
-XX:InitialCodeCacheSize | 配置CodeCache空间的初始值 |
-XX:ReservedCodeCache | 配置CodeCache空间的最大值 |
-XX:PrintCodeCache | 退出时输出CodeCache信息 |
3.4、编译阈值
触发代码编译的主要因素是代码执行的频度,当执行达到一定次数,且到达编译阈值,编译器就可获得足够的信息进行代码编译了。
编译时基于两种计数器:方法调用计数器和方法中循环回边计数器。回边为循环执行的次数。
虚拟机在执行某个方法时,会检查方法中的两种计数器总数,然后判断是否需要编译。需要编译则将该方法放入编译队列,进行标准编译。
标准编译由 -XX:CompileThreshold=N标志触发,N=回边计数器+方法调用次数。使用client编译器时,N默认值为1500,使用server编译器时为10000。
实际上每种计数器的值都会周期性减少,计数器只是方法或循环最新热度的度量。
3.5、编译线程
编译队列中的任务是被后台多个线程处理的。编译队列并不遵循先进先出的原则,而是根据调用次数的多少作为优先级。故当在出现开始执行并有大量代码需要编译时,最重要、执行次数最多的代码会被优先编译。
当使用 client 编译器时, JVM 会开启一个编译线程;使用 Server编译器时,JVM 会开启两个这样的线程。当启用分层编译时, JVM 默认开启多个 client 和 server 线程,线程数依据一个略复杂的等式而定,包括目标平台 CPU 数取双对数之后的数值。
编译器的线程数( 3 种编译器都是如此)可通过 -xx:CICompilerCount=N 标志来设置。这是JVM 处理队列的线程总数;对分层编译来说,其中三分之一(至少一个)将用来处理client 编译器队列,其余的线程(至少一个)用来处理 server编译器队列。
3.6、内联
编译器所做的最重要的优化方法内联。面向对象设计都会有很多getter和setter方法,此类方法调用开销很大,特别是相对于方法的代码量而言。当前的JVM都会用内联代码的方式执行这些方法。
内联是默认开启的,可通过 -xx:-InLine关闭,然而其对性能影响巨大,最好不要关闭。
方法是否内联取决干它有多热以及它的大小。 JVM 依据内部计算来判定方法是否是热点(譬如,调用很频繁);是否是热点并不直接与任何调优参数相关。如果方法因调用频繁而可以内联,那只有在它的字节码小于 325 字节时(或 -XX:MaxFreqInlineSize = N 所设定的任意值)才会内联。否则,只有方法很小时,即小于 35 字节(或 -xx : MaxlnlineSize = N 所设定的任意值)时才会内联。
有时你会看到增加MaxInlineSize 的值以便内联更多方法的建议。两者之间常被忽略的是,MaxInlineSize 超过35意味着第一次调用方法是就会被内联。然而,方法只有经常被调用时—在这种情况下它的性能会受更大影响― 最终才值得内联(假定它的大小小于 325 字节)。否则,MaxInlineSize调用的最终结果就是减少了热身测试所需要的时间,但不太可能对长期运行的程序产生重大影响。
3.7、逃逸分析
如果开启逃逸分析(-XX:DoEscapeAnalysist,默认为true),server编译器会执行一些非常激进的优化措施。
4、分层编译级别
当使用分层编译时,编译日志中会输出代码所编译的分层级别。
共5种编译级别:
- 0级:CompLevel_none,采用解释器解释执行,不采集性能监控数据,可以升级到1级;
- 1级:CompLevel_simple,采用C1编译器,会把热点代码迅速的编译成本地代码,如果需要可以采用性能数据;
- 2级:CompLevel_limited_profile,采用C2编译器,进行更好的优化,甚至可能根据第1级采集的性能数据采取激进的优化措施。
- 3级:CompLevel_full_profile,采用C1编译器,采集性能数据进行优化措施;
- 4级:CompLevel_full_optimization,采用C2编译器,进行完全的优化;
多数方法第一次编译级别是3,即完全C1编译,如果方法运行的足够频繁,它就会编译成4。
影响编译策略的因素有两个:
- C2 队列的长度决定了下一个等级.据观察,第 2 级比第 3 级快约 30 % ,因此我们需要将一个 Java 方法花费在第 3 级上的时间尽可能地最小化.所以,若 C2队列很长,直接选择第 3 级会导致排队,直到所提交的 C2 编译请求通历整个队列.因此此时较为明智的做法是先使用第 2 级,待 C2 负载回落,再启动第 3 级重新编译并开始收集性能数据。
- C1 队列的长度用来动态调整阈值,从而在编译器过载时引入额外的过滤.
如果server编译器队列满了,就会从 server 队列中取出方法,以级别 2 进行编译,在这个级别上, C1 编译器使用方法调用计数器和回边计数器(但不需要性能分析的反馈信息)。这使得方法编译得更快,而方法也将在 C1 编译器收集分析信息、之后被编译为级别 3 ,最终当 server 编译器队列不太忙的时候被编译为级别 4 。
另一方面,如果client 编译器全忙,原本排程在级别 3 编译的方法就既可以等待级别 3 编译,也适合进行级别 4 的编译。在这种情况下,方法编译会很快转到级别 2 ,然后由级别 2 转到级别 4 。
那些不太重要的方法可以从级别 2 或级别3 开始编译,但随后会因为它们的重要性没那么高而转为级别 1 。另外,如果 Server 编译器出于某些原因无法编译代码,也会转为级别 1。当然,代码在逆编译时会转为级别0 。
有些标志可以控制某些级别转换行为,但调优能够得到很乐观的结果。当方法按期望的顺序,即级别 0 —>级别 3 —>级别 4 编译时,性能可以达到最优。如果方法经常被编译为级别 2 ,并且还额外有可用的 CPU 周期,那就可以考虑增加编译器的线程数,从而减少 server编译器队列的长度。如果没有额外可用的 CPU 周期,那你唯一能做的就是尽力减小应用的大小。