解释执行与 JIT
Java 程序在运行的时候,主要就是执行字节码指令,一般这些指令会按照顺序解释执行,这种就是解释执行。那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码,如果按照解释执行,效率是非常低的。以上的这些代码称为热点代码。
所以,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT 编译器。
即时编译器类型
在 HotSpot 虚拟机中,内置了两个 JIT,分别为 C1 编译器和 C2 编译器。
C1 编译器
C1 编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序,例如,GUI 应用对界面启动速度就有一定要求,C1 也被称为 Client Compiler。
C2 编译器
C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。根据各自的适配性,这种即时编译也被称为 Server Compiler。
热点代码
热点代码,就是那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码。这些再次编译后的机器码会被缓存起来,以备下次使用,但对于那些执行次数很少的代码来说,这种编译动作就纯属浪费。
JVM 提供了一个参数“-XX:ReservedCodeCacheSize”,用来限制 CodeCache 的大小。也就是说,JIT 编译后的代码都会放在 CodeCache 里。
如果这个空间不足,JIT 就无法继续编译,编译执行会变成解释执行,性能会降低一个数量级。同时,JIT 编译器会一直尝试去优化代码,从而造成了 CPU占用上升。
通过 java -XX:+PrintFlagsFinal –version 查询:
![](https://img.haomeiwen.com/i24743904/bc27be9c0cd0a42e.png)
热点探测
(J9 使用过采样的热点探测技术,但是缺点是很难精确的确认一个方法的热度,所以这个技术没必要了解)
在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法” 虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。
在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。
方法调用计数器
用于统计方法被调用的次数,方法调用计数器的默认阈值在 客户端模式下是 1500 次,在服务端模式下是 10000 次(我们用的都是服务端,java –version查询),
可通过 -XX: CompileThreshold 来设定:
![](https://img.haomeiwen.com/i24743904/e0798d268015430e.png)
通过 java -XX:+PrintFlagsFinal –version 查询:
![](https://img.haomeiwen.com/i24743904/1264040fbb82d8be.png)
回边计数器
用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值,在不开启分层编译的情况下,在服务端模式下是 10700。
回边计数器阈值 =方法调用计数器阈值(CompileThreshold)×(OSR 比率(OnStackReplacePercentage)-解释器监控比率(InterpreterProfilePercentage)/100。
回边计数器阈值 =10000×(140-33)。
其中 OnStackReplacePercentage 默认值为 140,InterpreterProfilePercentage 默认值为 33,如果都取默认值,那 Server 模式虚拟机回边计数器的阈值为 10700。
通过 java -XX:+PrintFlagsFinal –version 查询:
![](https://img.haomeiwen.com/i24743904/56c903bc084777fc.png)
可通过 -XX: OnStackReplacePercentage=N 来设置:
建立回边计数器的主要目的是为了触发 OSR(On StackReplacement)编译,即栈上编译。在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直接将执行代码替换,执行缓存的机器语言。
JVM 的运用
在 Java7 之前,需要根据程序的特性来选择对应的 JIT,虚拟机默认采用解释器和其中一个编译器配合工作。
Java7 引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,我们也可以通过参数 “-client”“-server” 强制指定虚拟机的即时编译模式。
分层编译
而在分层编译的情况下,-XX: CompileThreshold 指定的阈值将失效,此时将会根据当前待编译的方法数以及编译线程数来动态调整。当方法计数器和回边计数器之和超过方法计数器阈值时,就会触发 JIT 编译器。
而在分层编译的情况下,-XX: OnStackReplacePercentage 指定的阈值同样会失效,此时将根据当前待编译的方法数以及编译线程数来动态调整。
在 Java8 中,默认开启分层编译。
通过 java -version 命令行可以直接查看到当前系统使用的编译模式(默认分层编译):
![](https://img.haomeiwen.com/i24743904/8749635ee267d520.png)
使用“-Xint”参数强制虚拟机运行于只有解释器的编译模式下:
![](https://img.haomeiwen.com/i24743904/0af6fc6289a6db5a.png)
使用“-Xcomp”强制虚拟机运行于只有 JIT 的编译模式下:
![](https://img.haomeiwen.com/i24743904/270b4eb0ff54d7b8.png)
JVM 的执行状态分为了 5 个层次:(不重要、了解即可)
第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译;
第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling;
第 2 层:也称为 C1 编译,开启 Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译;
第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译;
第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
编译优化技术
JIT 编译运用了一些经典的编译优化技术来实现代码的优化,即通过一些例行检查优化,可以智能地编译出运行时的最优性能代码.。
方法内联
方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。
例如以下方法:
![](https://img.haomeiwen.com/i24743904/1b880e49fe1b30f0.png)
最终会被优化为:
![](https://img.haomeiwen.com/i24743904/c696c2a64403d20a.png)
JVM 会自动识别热点方法,并对它们使用方法内联进行优化。
我们可以通过 -XX:CompileThreshold 来设置热点方法的阈值。
但要强调一点,热点方法不一定会被 JVM 做内联优化,如果这个方法体太大了,JVM 将不执行内联操作。
而方法体的大小阈值,我们也可以通过参数设置来优化:
经常执行的方法,默认情况下,方法体大小小于 325 字节的都会进行内联,我们可以通过 -XX:FreqInlineSize=N 来设置大小值:
![](https://img.haomeiwen.com/i24743904/2d0c5e9ad1de5cbd.png)
不是经常执行的方法,默认情况下,方法大小小于 35 字节才会进行内联,我们也可以通过 -XX:MaxInlineSize=N 来重置大小值:
![](https://img.haomeiwen.com/i24743904/765ceeff9e689acb.png)
代码演示
![](https://img.haomeiwen.com/i24743904/7b1364befe9d7833.png)
设置 VM 参数:-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
-XX:+PrintCompilation //在控制台打印编译过程信息;
-XX:+UnlockDiagnosticVMOptions //解锁对 JVM 进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对 JVM 进行诊断;
-XX:+PrintInlining //将内联方法打印出来。
![](https://img.haomeiwen.com/i24743904/9f6c1454889f9062.png)
如果循环太少,则不会触发方法内联。
![](https://img.haomeiwen.com/i24743904/2b232bcd88b233ea.png)
![](https://img.haomeiwen.com/i24743904/614a63f188496d41.png)
热点方法的优化可以有效提高系统性能,一般我们可以通过以下几种方式来提高方法内联:
通过设置 JVM 参数来减小热点阈值或增加方法体阈值,以便更多的方法可以进行内联,但这种方法意味着需要占用更多地内存;
在编程中,避免在一个方法中写大量代码,习惯使用小方法体;
尽量使用 final、private、static 关键字修饰方法,编码方法因为继承,会需要额外的类型检查。
锁消除
在非线程安全的情况下,尽量不要使用线程安全容器,比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 关键字修饰,会使用到锁,从而导致性能下降。
![](https://img.haomeiwen.com/i24743904/d77cb7241bf12732.png)
但实际上,在以下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。
![](https://img.haomeiwen.com/i24743904/1350abbccb6671f9.png)
下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。
![](https://img.haomeiwen.com/i24743904/24f1d4ced11173b5.png)
-XX:+EliminateLocks 开启锁消除(jdk1.8 默认开启,其它版本未测试);
-XX:-EliminateLocks 关闭锁消除;
我们把锁消除关闭---测试发现性能差别有点大。
![](https://img.haomeiwen.com/i24743904/bd9559c24a5429a0.png)
标量替换
逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替。
将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量替换(前提是需要开启逃逸分析)。
![](https://img.haomeiwen.com/i24743904/994b11f4b53403cf.png)
-XX:+DoEscapeAnalysis 开启逃逸分析(jdk1.8 默认开启);
-XX:-DoEscapeAnalysis 关闭逃逸分析;
-XX:+EliminateAllocations 开启标量替换(jdk1.8 默认开启);
-XX:-EliminateAllocations 关闭标量替换。
Java 的发展历史
1995 年 5 月 23 日,Sun 公司正式发布了 Java 语言和 HotJava 浏览器;
1996 年 1 月,Sun 公司发布了 Java 的第一个开发工具包(JDK 1.0);
1996 年 4 月,10 个最主要的操作系统供应商申明将在其产品中嵌入 Java 技术,发展可真是迅雷不及掩耳;
1996 年 9 月,大约 8.3 万个网页应用了 Java 技术来制作,这就是早年的互联网,即 Java Applet,真香定律;
1996 年 10 月,Sun 公司发布了 Java 平台第一个即时编译器(JIT),这一年很不平凡;
1997 年 2 月 18 日,JDK 1.1 面世,在随后的三周时间里,达到了 22 万次的下载量,PHP 甘拜下风;
1999 年 6 月,Sun 公司发布了第二代 Java 三大版本,即 J2SE、J2ME、J2EE,随之 Java2 版本发布;
2000 年 5 月 8 日,JDK 1.3 发布,四年升三版,不算过分哈;
2000 年 5 月 29 日,JDK 1.4 发布,获得 Apple 公司 Mac OS 的工业标准支持;
2001 年 9 月 24 日,Java EE 1.3 发布,注意是 EE,从此开始臃肿无比;
2002 年 2 月 26 日,J2SE 1.4 发布,自此 Java 的计算能力有了大幅度的提升,与 J2SE 1.3 相比,多了近 62% 的类与接口;
2004 年 9 月 30 日 18:00PM,J2SE 1.5 发布,1.5 正式更名为 Java SE 5.0;
2005 年 6 月,在 JavaOne 大会上,Sun 公司发布了 Java SE 6;
2009 年 4 月 20 日,Oracle 宣布收购 Sun,该交易的总价值约为 74 亿美元;
2010 年 Java 编程语言的创始人 James Gosling 从 Oracle 公司辞职,一朝天子一朝臣,国外也不例外;
2011 年 7 月 28 日,Oracle 公司终于发布了 Java 7,这次版本升级经过了将近 5 年时间;
2014 年 3 月 18 日,Oracle 公司发布了 Java 8,这次版本升级为 Java 带来了全新的 Lambda 表达式。
JDK 的发展(7 开始)
Java 7
Java 7 增加了以下新特性;
try、catch 能够捕获多个异常
新增 try-with-resources 语法
JSR341 脚本语言新规范
JSR203 更多的 NIO 相关函数
JSR292,课程中提到的 InvokeDynamic
支持 JDBC 4.1 规范
文件操作的 Path 接口、DirectoryStream、Files、WatchService
jcmd 命令
多线程 fork/join 框架
Java Mission Control
Java 8
Java 8 是一个重要的版本,在语法层面上有更大的改动,支持 Lamda 表达式,影响堪比 Java 5 的泛型支持:
支持 Lamda 表达式
支持集合的 stream 操作
提升了 HashMaps 的性能(红黑树)
提供了一系列线程安全的日期处理类
完全去掉了 Perm 区
Java 9
Java 9 增加了以下新特性:
JSR376 Java 平台模块系统
JEP261 模块系统
jlink 精简 JDK 大小
G1 成为默认垃圾回收器
CMS 垃圾回收器进入废弃倒计时
GC Log 参数完全改变,且不兼容
JEP110 支持 HTTP2,同时改进 HttpClient 的 API,支持异步模式
jshell 支持类似于 Python 的交互式模式
Java 10
Java 10 增加了以下新特性:
JEP304 垃圾回收器接口代码进行整改
JEP307 G1 在 FullGC 时采用并行收集方式
JEP313 移除 javah 命令
JEP317 重磅 JIT 编译器 Graal 进入实验阶段
Java 11
Java 11 增加了以下新特性:
JEP318 引入了 Epsilon 垃圾回收器(这个回收器什么都不干,适合短期任务)
JEP320 移除了 JavaEE 和 CORBA Modules,应该要走轻量级路线
Flight Recorder 功能,类似 JMC 工具里的功能
JEP321 内置 httpclient 功能,java.net.http 包
JEP323 允许 lambda 表达式使用 var 变量
废弃了 -XX+AggressiveOpts 选项
引入了 ZGC,依然是实验性质
Java 12
Java 12 增加了以下新特性:
JEP189 先加入 ShenandoahGC
JEP325 switch 可以使用表达式
JEP344 优化 G1 达成预定目标
优化 ZGC
Java 13
Java 13 增加了以下新特性:
JEP354 yield 替代 break
JEP355 加入了 Text Blocks,类似 Python 的多行文本
ZGC 的最大 heap 大小增大到 16TB
废弃 rmic Tool 并准备移除
Java 14
Java 14 增加了以下新特性:
JEP343 打包工具引入
JEP345 实现了 NUMA-aware 的内存分配,以提升 G1 在大型机器上的性能
JEP359 引入了 preview 版本的 record 类型,可用于替换 lombok 的部分功能
JEP364 之前的 ZGC 只能在 Linux 上使用,现在 Mac 和 Windows 上也能使用 ZGC 了
JEP363 正式移除 CMS,CMS 涉及到的一些优化参数,在 14 版本普及之后,将不复存在
总结
每一个版本的发布,Java 都会对以下进行改进:
优化垃圾回收器,减少停顿,提高吞吐
语言语法层面的升级
结构调整,减少运行环境的大小,模块化
废弃掉一些承诺要废弃的模块
Java 9 之后,已经进入了快速发布阶段,大约每半年发布一次,Java 8 和 Java 11 是目前支持的 LTS 版本(Long Term Support:长期演进版)。
网友评论