
在谈到 Java 的编译机制的时候,其实应该按时期,分为两个阶段。一个是 javac 指令将 Java 源码变为 Java 字节码的静态编译过程。另一个是 Java 字节码编译为本地机器码的过程,并且因为这个过程是在程序运行时期完成的所以称之为即时编译(JIT),下面我们讨论的编译也都是指“即时编译”过程。
java作为一种跨平台的语言实现了一次编译到处运行的特性,这也就决定了它编译出来的不是机器码而是特定的字节码。解释器(各平台不同)就是将字节码解释为机器指令,调用操作系统来完成程序的执行。
解释器虽然实现了跨平台的特性,但是解释执行的效率是很低的,是以牺牲性能为代价来换取的跨平台特性。所以 JVM 发现某个方法或者代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code,不知道Sun的虚拟机命名是否跟这个有联系)。为了提高热点代码的执行效率,在运行时,虚拟机就会将这些代码翻译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的就是编译器,被称为即时编译器(Just In Time Compiler,简称为JIT)。
HotSpot 虚拟机内置两个即时编译器,称为 Client Compiler 和 Server Compiler,分别简称为 C1,C2。
由于即时编译器编译本地代码需要占用程序运行时间,而要编译出优化程度较高的代码,所花费的时间可能更多。为了在程序启动速度与运行效率之间达到平衡,HotSpot 虚拟机启用了分层编译(Tiered Compilation)策略。
在分层编译中,会同时使用两个编译器。当 C2 编译器在等待并分析一些代码片段来收集信息的时候,C1 编译器首先开始编译。这使得 C1 编译器能够快速的提高性能;而 C2 编译器将能够更好地提高性能,因为它拥有有热点方法更好的信息。分层编译在 JDK1.6 时期出现,在 JDK1.7 的 Server 模式中作为默认编译策略开启。
根据编译器编译、优化的规模耗时,划分出不同的编译级别:
| Level | Compiler |
|---|---|
| 0 | 仅解释执行 |
| 1 | 执行不带 profiling 的 C1 代码 |
| 2 | 执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码 |
| 3 | 执行带所有 profiling 的 C1 代码 |
| 4 | 执行 C2 代码 |
profiling 就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。
通常情况下,C2 代码的执行效率要比 C1 代码的高出 30% 以上。对于 C1 代码的三种状态,按执行效率从高至低则是 1 层 > 2 层 > 3 层。其中 1 层的性能比 2 层的稍微高一些,而 2 层的性能又比 3 层高出 30%。这是因为 profiling 越多,其额外的性能开销越大。
这 5 个层次的执行状态中,1 层和 4 层为终止状态。当一个方法被终止状态编译过后,如果编译后的代码并没有失效,那么 Java 虚拟机将不再次发出该方法的编译请求的。

上图列举了一些编译的路径。
通常情况下,热点方法会经过 3 层的 C1 编译,然后再被 4 层的 C2 编译。
如果方法的字节码数目比较少(如 getter/setter),而且 3 层的 profiling 没有可收集的数据。那么 JVM 断定该方法对于 C1 代码和 C2 代码的执行效率相同。在这种情况下,Java 虚拟机会在 3 层编译之后,直接选择用 1 层的 C1 编译。由于这是一个终止状态,因此 Java 虚拟机不会继续用 4 层的 C2 编译。
默认启用的是混合模式(解释器与编译器配合工作)
可以使用 -Xint 参数强制虚拟机运行于只有解释器模式下
可以使用 -Xcomp 强制虚拟机运行于只有 JIT 的编译模式下
Java8 中默认开启分层编译 -client,-server 参数已经无效,如果只想开启 C2,可以关闭分层编译(-XX:-TieredCompilation)
如果只想开启 C1,可以在打开分层编译的同时,使用参数:-XX:TieredStopAtLevel=1。
JIT 编译器基于一个非常基本的原则:编译和优化执行频率更高的代码段。如果代码很少执行,即使优化之后提升 80% 的速度也是没有必要的。可以说热点代码是 JIT 编译的前提,而热点代码的判定就是基于热点探测技术。
主要是虚拟机会周期性的检查各个线程的栈顶,若某个或某些方法经常出现在栈顶,那这个方法就是“热点方法”。
优点是实现简单。
缺点是很难精确一个方法的热度,容易受到线程阻塞或外界因素的影响。
主要就是虚拟机给每一个方法甚至代码块建立了一个计数器,统计方法的执行次数,超过一定的阀值则标记为此方法为热点方法。
HotSpot 虚拟机使用的基于计数器的热点探测方法。然后使用了两类计数器:方法调用计数器和回边计数器。当方法计数器和回边计数器之和超过方法计数器阈值时,就会触发JIT编译器。
在编译时,将方法调用优化为直接使用方法体中的代码进行替换,这就是方法内联,这样做减少了方法调用过程中压栈与出栈的开销,同时也为之后的一些优化手段提供条件。
@Benchmark
public int inline() {
CounterObj counterObj = new CounterObj();
counterObj.add(1);
counterObj.add(2);
return counterObj.getCounter();
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int dontInline() {
CounterObj counterObj = new CounterObj();
counterObj.add(1);
counterObj.add(2);
return counterObj.getCounter();
}
public static class CounterObj {
@Getter
private int counter;
public void add(int num) {
this.counter = sum(this.counter, num);
}
public int sum(int a, int b) {
return a + b;
}
}
------------------------------------------------------------------------
Benchmark Mode Cnt Score Error Units
MethodInline.dontInline avgt 5 3.936 ± 0.127 ns/op
MethodInline.inline avgt 5 2.620 ± 0.042 ns/op
如果一个变量的使用,在运行期检测它的作用范围不会超过一个方法或者一个线程的作用域。那么这个变量就不会被多个线程所共享,也就是说可以不将其分配在堆空间中,而是将其线程私有化。
如何来检测一个变量的作用域仅在一个方法或者线程中呢? JVM 中使用全局数据流分析机制实现的一种机制,称之为逃逸分析,作为其他一些激进优化的前提条件。
可以通过 -XX:+DoEscapeAnalysis 开启逃逸分析(jdk8中默认开启),-XX:-DoEscapeAnalysis 来关闭逃逸分析。下面是基于逃逸分析基础上做的一些优化。
当对象不会被外部访问,并且对象可以被进一步分解时,JVM 不会创建该对象,而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这个过程就是标量替换。对象将跟随栈的创建而创建,销毁而销毁,减轻了 GC 的负担以及工作内存跟主存的同步消耗。
很多人会把标量替换跟栈上分配拆开来解释,但我认为标量替换跟栈上分配说的是一件事情。因为在栈上是不能创建对象的(栈上只能存放一些基本类型以及对象的引用),只有进行了标量替换,将聚合量拆分为标量之后才达成栈上分配的目的。
可以通过 -XX:+EliminateAllocations 开启标量替换(jdk8 中默认开启),-XX:-EliminateAllocations 来关闭标量替换。
@Benchmark
@Fork(jvmArgsAppend = "-XX:+EliminateAllocations")
public void escaped() {
methodA();
}
@Benchmark
@Fork(jvmArgsAppend = "-XX:-EliminateAllocations")
public void noEscape() {
methodA();
}
public void methodA() {
new Tmp();
}
@Data
public static class Tmp {
private int data;
}
------------------------------------------------------------------------
Benchmark Mode Cnt Score Error Units
ScalarReplace.escaped avgt 5 0.354 ± 0.055 ns/op
ScalarReplace.noEscape avgt 5 2.661 ± 0.264 ns/op
当加锁的变量不会发生逃逸,是线程私有的时候,那么完全没有必要加锁。 在 JIT 时期就可以将同步锁去掉,以减少加锁与解锁造成的资源开销。
@Benchmark
@Fork(jvmArgsAppend = "-XX:+EliminateLocks")
public void escaped() {
methodA();
}
@Benchmark
@Fork(jvmArgsAppend = "-XX:-EliminateLocks")
public void noEscape() {
methodA();
}
public void methodA() {
synchronized (new Object()) {
// do nothing
}
}
------------------------------------------------------------------------
Benchmark Mode Cnt Score Error Units
LockRemove.escaped avgt 5 0.357 ± 0.053 ns/op
LockRemove.noEscape avgt 5 21.847 ± 0.236 ns/op
除了上面举例的几种经典优化方式,JVM 还为我们执行很多其他优化,如:无用代码消除(Dead Code Elimination)、循环展开(Loop Unrolling)、循环表达式外提(Loop Expression Hoisting)、消除公共子表达式(Common Subexpression Elimination)、常量传播(Constant Propagation)、基本块冲排序(Basic Block Reordering)等。
经过辛苦的编译优化之后的本地代码是比较珍贵的,这些代码会被缓存起来,当下一次运行的时候就可以直接使用了,也就是所谓的代码缓存(Code Cache)。在 32 位机器client模式默认 32MB,64 位机器默认 240MB。可以使用- XX:InitialCodeCacheSize,-XX:ReservedCodeCacheSize 来修改代码缓存的大小。
代码缓存很少引起性能问题,但是一旦发生其影响可能是毁灭性的。如果代码缓存被占满,JVM 会打印出一条警告消息,并切换到 interpreted-only 模式:JIT 编译器被停用,字节码将不再会被编译成机器码。应用程序将继续运行,但运行速度会降低一个数量级,直到有人注意到这个问题。
通过设置 -XX:+UseCodeCacheFlushing 这个参数,当代码缓存满了的时候,会让 JVM 换出一部分缓存以容纳新编译的代码,避免直接进入解释模式使性能急剧下降。在默认情况下,这个选项是关闭的。
由于编译情况复杂,JVM 也会动态调整相关的阈值来保证 JVM 的性能,所以不建议手动调整编译相关的参数。除非一些特定的 Case,比如 CodeCache 满了停止编译,可以适当增加 CodeCache 大小。或者一些非常常用的方法,未被内联到而拖累了性能,可以调整内敛层数或者内联方法的大小来解决。

上图是一段编译的信息输出,从左到右依次是:
有五种不同类型的属性来表示编译的状态。
% - The compilation is OSR (on-stack replacement).
s - The method is synchronized.
! - The method has an exception handler.
b - Compilation occurred in blocking mode.
n - Compilation occurred for a wrapper to a native method.
该字段通常具有以下两个值之一:“made not entrant”或“made zombie”。
我们可以看到为了让我们的代码跑的更快,JVM 默默为我们做了很多的事情,但是凡事都是有利有弊。比如一个 QPS 较高的应用,重启之后如果没有比较好的预热策略,可能就会因为分层编译导致接口响应变慢,CPU 飙升等问题。
深入理解Java虚拟机--周志明
我正在使用Ruby解决一些ProjectEuler问题,特别是这里我要讨论的问题25(Fibonacci数列中包含1000位数字的第一项的索引是多少?)。起初,我使用的是Ruby2.2.3,我将问题编码为:number=3a=1b=2whileb.to_s.length但后来我发现2.4.2版本有一个名为digits的方法,这正是我需要的。我转换为代码:whileb.digits.length当我比较这两种方法时,digits慢得多。时间./025/problem025.rb0.13s用户0.02s系统80%cpu0.190总计./025/problem025.rb2.19s用户0.0
我正在寻找一个用ruby演示计时器的在线示例,并发现了下面的代码。它按预期工作,但这个简单的程序使用30Mo内存(如Windows任务管理器中所示)和太多CPU有意义吗?非常感谢deftime_blockstart_time=Time.nowThread.new{yield}Time.now-start_timeenddefrepeat_every(seconds)whiletruedotime_spent=time_block{yield}#Tohandle-vesleepinteravalsleep(seconds-time_spent)iftime_spent
如果用户是所有者,我有一个条件来检查说删除和文章。delete_articleifuser.owner?另一种方式是user.owner?&&delete_article选择它有什么好处还是它只是一种写作风格 最佳答案 性能不太可能成为该声明的问题。第一个要好得多-它更容易阅读。您future的自己和其他将开始编写代码的人会为此感谢您。 关于ruby-on-rails-如果条件与&&,是否有任何性能提升,我们在StackOverflow上找到一个类似的问题:
我编写了一个Ruby应用程序,它可以解析来自不同格式html、xml和csv文件的源中的大量数据。我如何找出代码的哪些区域花费的时间最长?有没有关于如何提高Ruby应用程序性能的好资源?或者您是否有任何始终遵循的性能编码标准?例如,你总是用加入你的字符串吗?output=String.newoutput或者你会使用output="#{part_one}#{part_two}\n" 最佳答案 好吧,有一些众所周知的做法,例如字符串连接比“#{value}”慢得多,但是为了找出您的脚本在哪里消耗了大部分时间或比所需时间更多,您需要进行分
目录0专栏介绍1平面2R机器人概述2运动学建模2.1正运动学模型2.2逆运动学模型2.3机器人运动学仿真3动力学建模3.1计算动能3.2势能计算与动力学方程3.3动力学仿真0专栏介绍?附C++/Python/Matlab全套代码?课程设计、毕业设计、创新竞赛必备!详细介绍全局规划(图搜索、采样法、智能算法等);局部规划(DWA、APF等);曲线优化(贝塞尔曲线、B样条曲线等)。?详情:图解自动驾驶中的运动规划(MotionPlanning),附几十种规划算法1平面2R机器人概述如图1所示为本文的研究本体——平面2R机器人。对参数进行如下定义:机器人广义坐标
网站的日志分析,是seo优化不可忽视的一门功课,但网站越大,每天产生的日志就越大,大站一天都可以产生几个G的网站日志,如果光靠肉眼去分析,那可能看到猴年马月都看不完,因此借助网站日志分析工具去分析网站日志,那将会使网站日志分析工作变得更简单。下面推荐两款网站日志分析软件。第一款:逆火网站日志分析器逆火网站日志分析器是一款功能全面的网站服务器日志分析软件。通过分析网站的日志文件,不仅能够精准的知道网站的访问量、网站的访问来源,网站的广告点击,访客的地区统计,搜索引擎关键字查询等,还能够一次性分析多个网站的日志文件,让你轻松管理网站。逆火网站日志分析器下载地址:https://pan.baidu.
LL库和HAL库简介LL:Low-Layer,底层库HAL:HardwareAbstractionLayer,硬件抽象层库LL库和hal库对比,很精简,这实际上是一个精简的库。LL库的配置选择如下:在STM32CUBEMX中,点击菜单的“ProjectManager”–>“AdvancedSettings”,在下面的界面中选择“AdvancedSettings”,然后在每个模块后面选择使用的库总结:1、如果使用的MCU是小容量的,那么STM32CubeLL将是最佳选择;2、如果结合可移植性和优化,使用STM32CubeHAL并使用特定的优化实现替换一些调用,可保持最大的可移植性。另外HAL和L
一、机器人介绍 此处是基于MATLABRVC工具箱,对ABB-IRB-1200型号的微型机械臂进行正逆向运动学分析,并利Simulink工具实现对机械臂进行具有动力学参数的末端轨迹规划仿真,最后根据机械模型设计Simulink-Adams联合仿真。 图1.ABBIRB 1200尺寸参数示意图ABBIRB 1200提供的两种型号广泛适用于各作业,且两者间零部件通用,两种型号的工作范围分别为700 mm 和 900 mm,大有效负载分别为 7 kg 和5 kg。 IRB 1200 能够在狭小空间内能发挥其工作范围与性能优势,具有全新的设计、小型化的体积、高效的性能、易于集成、便捷的接
目录一.大致如下常见问题:(1)找不到程序所依赖的Qt库version`Qt_5'notfound(requiredby(2)CouldnotLoadtheQtplatformplugin"xcb"in""eventhoughitwasfound(3)打包到在不同的linux系统下,或者打包到高版本的相同系统下,运行程序时,直接提示段错误即segmentationfault,或者Illegalinstruction(coredumped)非法指令(4)ldd应用程序或者库,查看运行所依赖的库时,直接报段错误二.问题逐个分析,得出解决方法:(1)找不到程序所依赖的Qt库version`Qt_5'
我想使用ruby-prof和JMeter分析Rails应用程序。我对分析特定Controller/操作/或模型方法的建议方法不感兴趣,我想分析完整堆栈,从上到下。所以我运行这样的东西:RAILS_ENV=productionruby-prof-fprof.outscript/server>/dev/null然后我在上面运行我的JMeter测试计划。然而,问题是使用CTRL+C或SIGKILL中断它也会在ruby-prof可以写入任何输出之前杀死它。如何在不中断ruby-prof的情况下停止mongrel服务器? 最佳答案