JVM-编译-学习笔记
2018-06-29 本文已影响30人
HardWJJ
JAVA编译
将java源代码编译成机器指令经过以下步骤(根据完成任务不同,可以将编译器的组成部分划分为前端与后端)
QQ20180414-203816.png
前端编译(词法分析、语法分析、语义分析与中间代码生成)
.java文件编译成.class的编译过程,主要指与源语言有关但与目标机无关的部分。
- 词法分析
编译过程的第一个阶段。这个阶段的任务是从左到右一个字符一个字符地读入源程序,将字符序列转换为标记序列并对标记进行分类的过程。 - 语法分析
在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,源程序在结构上是否正确。 - 语义分析
在语法分析的基础上对结构上正确的源程序进行上下文有关性质的审查,进行类型审查(重要部分)。审查源程序有无语义错误,为代码生成阶段收集类型信息。 - 中间代码生成
在源程序的语法分析和语义分析完成之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。在Java中,javac执行的结果就是得到一个字节码(包括解语法糖),而这个字节码就是一种中间代码。
后端编译(代码优化和目标代码生成等)
JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。但执行速度必然会比可执行的二进制字节码程序慢很多,为了解决这种效率问题,引入了 JIT 技术。
当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用。
JIT编译器
JIT 是 just in time 的缩写, 也就是即时编译编译器。使用即时编译器技术,能够加速 Java 程序的执行速度。
JIT编译过程
-
JVM 读入.class 文件解释后,将其发给 JIT 编译器。JIT 编译器将字节码编译成本机机器代码,下图展示了该过程。
img001.png
自适应的即时编译和运行时优化
- Hot spot将字节码转换为机器码的第一步就是解释,解释在JVM启动时开始,是字节码最慢的执行形式。为了更快更有效地生成机器码,运行时会启动即时编译器。
- 即时编译器是一个自适应优化器,针对已证明为性能关键的方法予以优化。为了确定这些性能关键的方法,JVM会针对以下关键指标持续监控这些代码:1、方法进入计数,为每个方法分配一个调用计数器。2、循环分支计数,为每个已执行的循环分配一个计数器。
- 如果一个具体方法的方法进入计数和循环边计数超过了由运行时设定的编译临界值,或者循环的循环分支计数超过了之前已经指定的临界值,则认定它为性能关键的方法。
- OpenJDK HotSpot VM有两个不同的编译器,每个都有它自己的编译临界值:
1、客户端或C1编译器,它的编译临界值比较低,只是1500,这有助于减少启动时间。
2、服务端或C2编译器,它的编译临界值比较高,达到了10000,这有助于针对性能关键的方法生成高度优化的代码,这些方法由应用的关键执行路径来判定是否属于性能关键方法。
分层编译
- 通过引进分层编译,OpenJDK HotSpot VM 用户可以通过使用服务端编译器改进启动时间获得好处。
- 当使用客户端编译时,代码在启动期间通过客户端编译器予以优化,生成比解释型代码更好的性能优化信息。编译的代码存在在一个称为“代码缓存”的缓存里。代码缓存有固定的大小,如果满了,JVM将停止方法编译。
- 分层编译可以针对每一层设定它自己的临界值,比如-XX:Tier3MinInvocationThreshold, -XX:Tier3CompileThreshold, -XX:Tier3BackEdgeThreshold。第三层最低调用临界值为100。而未分层的C1的临界值为1500,与之对比你会发现会非常频繁地发生分层编译,针对客户端编译的方法生成了更多的性能分析信息。于是用于分层编译的代码缓存必须要比用于不分层的代码缓存大得多,所以在OpenJDK中用于分层编译的代码缓存默认大小为240MB,而用于不分层的代码缓存大小默认只有48MB。
Hot Spot 编译
- 当 JVM 执行代码时,它并不立即开始编译代码。这主要有两个原因:
1、JIT编译器可以权衡代码的执行频率,在java代码编译为字节码文件后,JIT可以将运行频率高的字节码(热点代码)直接编译成及器指令并保存供下一次使用以提高性能。
2、当 JVM 执行某一方法或遍历循环的次数越多,就会更加了解代码结构,那么 JVM 在编译代码的时候就做出相应的优化,减少解释器执行字节码文件动态查询的时间。
例子:equals() 这个方法存在于每一个 Java Object 中而且经常被覆写。当解释器遇到 b = obj1.equals(obj2) 这样一句代码,它则会查询 obj1 的类型从而得知到底运行哪一个 equals() 方法。
初级调优:客户模式或服务器模式
- JIT编译器在运行程序的时候有两种编译模式,在运行时会使用其中一种以达到最优性能。
1、Server模式:
启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。使用的是一个代号为 C2 的编译器。
2、Client模式:
使用的是一个代号为 C1 的轻量级编译器。
3、C2 比 C1 编译器编译的相对彻底,服务起来之后,性能更高。通过 java -version 命令行可以直接查看当前系统使用的编译模式。
中级编译器调优
- 优化代码缓存
当 JVM 编译代码时,它会将汇编指令集保存在代码缓存。代码缓存具有固定的大小,并且一旦它被填满,JVM 则不能再编译更多的代码。
1、通过 –XX:ReservedCodeCacheSize=Nflag(N 就是之前提到的默认大小)来最大化代码缓存大小。代码缓存的管理类似于 JVM 中的内存管理:有一个初始大小(用-XX:InitialCodeCacheSize=N 来声明)。代码缓存的大小从初始大小开始,随着缓存被填满而逐渐扩大。重定义代码缓存的大小并不会真正影响性能。
2、代码缓存并不是无限的,但是为代码缓存设置一个很大的值并不会对应用程序本身造成影响,应用程序并不会内存溢出,这些额外的内存预定会被操作系统所接受的。 - 编译阈值
在 JVM 中,编译是基于两个计数器的:一个是方法被调用的次数,另一个是方法中循环被回弹执行的次数。当 JVM 执行一个 Java 方法,它会检查这两个计数器的总和以决定这个方法是否有资格被编译。
1、当方法里有一个很长的循环或者是一个永远都不会退出并提供了所有逻辑的程序,JVM会在每执行完一次循环,分支计数器都会自增和自检。如果分支计数器计数超出其自身阈值,那么这个循环将具有被编译资格。
2、标准编译是被-XX:CompileThreshold=Nflag 的值所触发。Client 编译器模式下,N 默认的值 1500,而 Server 编译器模式下,N 默认的值则是 10000。改变 CompileThreshold 标志的值将会使编译器相对正常情况下提前(或推迟)编译代码。 - 检查编译过程
1、启用PrintCompilation,每次一个方法(或循环)被编译,JVM 都会打印出刚刚编译过的相关信息。
2、用 jstat 命令检查编译,Jstat 有两个选项可以提供编译器信息。其中,-compile 选项提供总共有多少方法被编译的总结信息(下面 6006 是要被检查的程序的进程 ID):进程详情 % jstat -compiler 6006 CompiledFailedInvalid TimeFailedTypeFailedMethod 206 0 0 1.97 0
高级编译器调优
当一个方法拥有编译资格时,它就会排队并等待编译。这个队列是由一个或很多个后台线程组成。编译是一个异步的过程。并且这些队列并不会严格的遵守先进先出原则:哪一个方法的调用计数器计数更高,哪一个就拥有优先权,这种优先权顺序保证最重要的代码被优先编译。
逃逸分析
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。--《深入理解Java虚拟机中》
- 逃逸分析对代码的优化
1、如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
2、将堆分配转化为栈分配。
3、对象的一部分可以不存储在内存,而是存储在CPU寄存器中。 - 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
- 方法逃逸:当一个对象在方法中被定义后,它可能被外部方法所引用。
例如:下面代码,StringBuffer sb是一个方法内部变量,下面代码中直接将sb返回,导致StringBuffer sb可能被外部方法改变。
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
- 随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配,但还是有部分对象会在堆内存中分配。