learn.lianglianglee.com/专栏/Java 性能优化实战-完/18 高级进阶:JIT 如何影响 JVM 的性能?.md.html
2022-09-06 22:30:37 +08:00

422 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>18 高级进阶JIT 如何影响 JVM 的性能?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/Java 性能优化实战-完/00 Java 性能优化,是进阶高级架构师的炼金石.md.html">00 Java 性能优化,是进阶高级架构师的炼金石</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/01 理论分析:性能优化,有哪些衡量指标?需要注意什么?.md.html">01 理论分析:性能优化,有哪些衡量指标?需要注意什么?</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/02 理论分析:性能优化有章可循,谈谈常用的切入点.md.html">02 理论分析:性能优化有章可循,谈谈常用的切入点</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/03 深入剖析:哪些资源,容易成为瓶颈?.md.html">03 深入剖析:哪些资源,容易成为瓶颈?</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/04 工具实践:如何获取代码性能数据?.md.html">04 工具实践:如何获取代码性能数据?</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/05 工具实践:基准测试 JMH精确测量方法性能.md.html">05 工具实践:基准测试 JMH精确测量方法性能</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/06 案例分析:缓冲区如何让代码加速.md.html">06 案例分析:缓冲区如何让代码加速</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/07 案例分析:无处不在的缓存,高并发系统的法宝.md.html">07 案例分析:无处不在的缓存,高并发系统的法宝</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/08 案例分析Redis 如何助力秒杀业务.md.html">08 案例分析Redis 如何助力秒杀业务</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/09 案例分析:池化对象的应用场景.md.html">09 案例分析:池化对象的应用场景</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/10 案例分析:大对象复用的目标和注意点.md.html">10 案例分析:大对象复用的目标和注意点</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/11 案例分析:如何用设计模式优化性能.md.html">11 案例分析:如何用设计模式优化性能</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/12 案例分析:并行计算让代码“飞”起来.md.html">12 案例分析:并行计算让代码“飞”起来</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/13 案例分析:多线程锁的优化.md.html">13 案例分析:多线程锁的优化</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/14 案例分析:乐观锁和无锁.md.html">14 案例分析:乐观锁和无锁</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/15 案例分析:从 BIO 到 NIO再到 AIO.md.html">15 案例分析:从 BIO 到 NIO再到 AIO</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/16 案例分析:常见 Java 代码优化法则.md.html">16 案例分析:常见 Java 代码优化法则</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/17 高级进阶JVM 如何完成垃圾回收?.md.html">17 高级进阶JVM 如何完成垃圾回收?</a>
</li>
<li>
<a class="current-tab" href="/专栏/Java 性能优化实战-完/18 高级进阶JIT 如何影响 JVM 的性能?.md.html">18 高级进阶JIT 如何影响 JVM 的性能?</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/19 高级进阶JVM 常见优化参数.md.html">19 高级进阶JVM 常见优化参数</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/20 SpringBoot 服务性能优化.md.html">20 SpringBoot 服务性能优化</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/21 性能优化的过程方法与求职面经总结.md.html">21 性能优化的过程方法与求职面经总结</a>
</li>
<li>
<a href="/专栏/Java 性能优化实战-完/22 结束语 实践出真知.md.html">22 结束语 实践出真知</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>18 高级进阶JIT 如何影响 JVM 的性能?</h1>
<p>我们在上一课时,我们了解到 Java 虚拟机栈,其实是一个双层的栈,如下图所示,第一层就是针对 method 的栈帧,第二层是针对字节码指令的操作数栈。</p>
<p><img src="assets/CgqCHl9YevCARjawAAC9Bvf_IoE321.png" alt="png" /></p>
<p>Java 虚拟机栈图</p>
<p>栈帧的创建是需要耗费资源的,尤其是对于 Java 中常见的 getter、setter 方法来说,这些代码通常只有一行,每次都创建栈帧的话就太浪费了。</p>
<p>另外Java 虚拟机栈对代码的执行,采用的是字节码解释的方式,考虑到下面这段代码,变量 a 声明之后,就再也不被使用,要是按照字节码指令解释执行的话,就要做很多无用功。</p>
<pre><code>public class A{
int attr = 0;
public void test(){
int a = attr;
System.out.println(&quot;ok&quot;);
}
}
</code></pre>
<p>下面是这段代码的字节码指令,我们能够看到 aload_0getfield istore_1 这三个无用的字节码指令操作。</p>
<pre><code> public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: getfield #2 // Field attr:I
4: istore_1
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String ok
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: return
LineNumberTable:
line 4: 0
line 5: 5
line 6: 13
</code></pre>
<p>另外我们了解到垃圾回收器回收的目标区域主要是堆堆上创建的对象越多GC 的压力就越大。要是能把一些变量,直接在栈上分配,那 GC 的压力就会小一些。</p>
<p>其实我们说的这几个优化的可能性JVM 已经通过 JIT 编译器Just In Time Compiler去做了JIT 最主要的目标是把解释执行变成编译执行。</p>
<p>为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,这就是 JIT 编译器的功能。</p>
<p><img src="assets/CgqCHl9YeyCAWROAAABdUyAOP5E893.png" alt="png" /></p>
<p>如上图JVM 会将调用次数很高,或者在 for 循环里频繁被使用的代码,编译成机器码,然后缓存在 CodeCache 区域里,下次调用相同方法的时候,就可以直接使用。</p>
<p>那 JIT 编译都有哪些手段呢?接下来我们详细介绍。</p>
<h3>方法内联</h3>
<p><strong>“05 | 工具实践:基准测试 JMH精确测量方法性能”</strong> 提到 JMH 的时候,我们就了解到 CompilerControl 注解可以控制 JIT 编译器的一些行为。</p>
<p>其中,有一个模式叫作<strong>inline</strong>,就是内联的意思,它会把一些短小的方法体,直接纳入目标方法的作用范围之内,就像是直接在代码块中追加代码。这样,就少了一次方法调用,执行速度就能够得到提升,这就是方法内联的概念。</p>
<p>可以使用 -XX:-Inline 参数来禁用方法内联,如果想要更细粒度的控制,可以使用 CompileCommand 参数,例如:</p>
<pre><code>-XX:CompileCommand=exclude,java/lang/String.indexOf
</code></pre>
<p>JMH 就是使用这个参数来实现的自定义编译特性。
在 JDK 的源码里,也有很多被 <strong>@ForceInline</strong>注解的方法,这些方法,会在执行的时候被强制进行内联;而被**@DontInline**注解的方法,则始终不会被内联。</p>
<p>我们从 <strong>“05 | 工具实践:基准测试 JMH精确测量方法性能”</strong> 获取第 16 个代码示例,来看一下 JIT 这些优化的效果,主要代码块如下:</p>
<pre><code>public void target_blank() {
// this method was intentionally left blank
}
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void target_dontInline() {
// this method was intentionally left blank
}
@CompilerControl(CompilerControl.Mode.INLINE)
public void target_inline() {
// this method was intentionally left blank
}
@CompilerControl(CompilerControl.Mode.EXCLUDE)
public void target_exclude() {
// this method was intentionally left blank
}
</code></pre>
<p>执行结果如下,可以看到不使用 JIT 编译和使用了 JIT 编译的性能差距达到了 100 多倍,使用了内联比不使用内联,速度快了 5 倍。</p>
<pre><code>Benchmark Mode Cnt Score Error Units
JMHSample_16_CompilerControl.baseline avgt 3 0.485 ± 1.492 ns/op
JMHSample_16_CompilerControl.blank avgt 3 0.483 ± 1.518 ns/op
JMHSample_16_CompilerControl.dontinline avgt 3 1.934 ± 3.112 ns/op
JMHSample_16_CompilerControl.exclude avgt 3 57.603 ± 4.435 ns/op
JMHSample_16_CompilerControl.inline avgt 3 0.483 ± 1.520 ns/op
</code></pre>
<p><img src="assets/CgqCHl9Ye0KASdaPAAFd8UGlCRY151.png" alt="png" /></p>
<p>JIT 编译之后的二进制代码,是放在 Code Cache 区域里的。这个区域的大小是固定的,而且一旦启动无法扩容。如果 Code Cache 满了JVM 并不会报错但会停止编译。所以编译执行就会退化为解释执行性能就会降低。不仅如此JIT 编译器会一直尝试去优化你的代码,造成 CPU 占用上升。</p>
<p>通过参数 -XX:ReservedCodeCacheSize 可以指定 Code Cache 区域的大小,如果你通过监控发现空间达到了上限,就要适当的增加它的大小。</p>
<h3>编译层次</h3>
<p>HotSpot 虚拟机包含多个即时编译器,有 C1C2 和 GraalJDK8 以后采用的是分层编译的模式。使用 jstack 命令获得的线程信息,经常能看到它们的身影。</p>
<pre><code>&quot;C2 CompilerThread0&quot; #6 daemon prio=9 os_prio=31 cpu=830.41ms elapsed=4252.14s tid=0x00007ffaed023000 nid=0x5a03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
No compile task
&quot;C1 CompilerThread0&quot; #8 daemon prio=9 os_prio=31 cpu=549.91ms elapsed=4252.14s tid=0x00007ffaed831800 nid=0x5c03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
No compile task
</code></pre>
<p>使用额外线程进行即时编译可以不用阻塞解释执行的逻辑。JIT 通常会在触发之后就在后台运行,编译完成之后就将相应的字节码替换为编译后的代码。
JIT 编译方式有两种:一种是编译方法,另一种是编译循环。</p>
<p>分层编译将 Java 虚拟机的执行状态分为了五个层次。</p>
<ul>
<li>字节码的解释执行;</li>
<li>执行不带 profiling 的 C1 代码;</li>
<li>执行仅带方法调用次数以及循环执行次数 profiling 的 C1 代码;</li>
<li>执行带所有 profiling 的 C1 代码;</li>
<li>执行 C2 代码。</li>
</ul>
<p>其中Profiling 指的是运行时的程序的执行状态数据,比如循环调用的次数、方法调用的次数、分支跳转次数、类型转换次数等。比如 JDK 中的 hprof 工具,就是一种 profiler说白了就是一些中间的统计数据。</p>
<p>在不启用分层编译的情况下,当方法的调用次数和循环回边的次数的总和,超过由参数 -XX:CompileThreshold 指定的阈值时,便会触发即时编译;但当启用分层编译时,这个参数将会失效,会采用一套动态调整进行调整。</p>
<h3>逃逸分析</h3>
<p><strong>下面着重讲解一下逃逸分析,这个知识点在面试的时候经常会被问到。</strong></p>
<p>我们先回顾一下上一课时留下的问题:我们常说的对象,除了基本数据类型,一定是在堆上分配的吗?</p>
<p>答案是否定的通过逃逸分析JVM 能够分析出一个新的对象的使用范围,从而决定是否要将这个对象分配到堆上。逃逸分析现在是 JVM 的默认行为,可以通过参数 -XX:-DoEscapeAnalysis 关掉它。</p>
<p>那什么样的对象算是逃逸的呢?可以看一下下面的两种典型情况。</p>
<p>如代码所示,对象被赋值给成员变量或者静态变量,可能被外部使用,变量就发生了逃逸。</p>
<pre><code>public class EscapeAttr {
Object attr;
public void test() {
attr = new Object();
}
}
</code></pre>
<p>再看下面这段代码,对象通过 return 语句返回。由于程序并不能确定这个对象后续会不会被使用,外部的线程能够访问到这个结果,对象也发生了逃逸。</p>
<pre><code>public class EscapeReturn {
Object attr;
public Object test() {
Object obj = new Object();
return obj;
}
}
</code></pre>
<p>那逃逸分析有什么好处呢?
<strong>1. 栈上分配</strong></p>
<p>如果一个对象在子程序中被分配,指向该对象的指针永远不会逃逸,对象有可能会被优化为栈分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再分配到堆空间,可以有效地减少 GC 的压力。</p>
<p><strong>2. 分离对象或标量替换</strong></p>
<p>但对象结构通常都比较复杂,如何将对象保存在栈上呢?</p>
<p>JIT 可以将对象打散,全部替换为一个个小的局部变量,这个打散的过程,就叫作标量替换(标量就是不能被进一步分割的变量,比如 int、long 等基本类型)。也就是说,标量替换后的对象,全部变成了局部变量,可以方便地进行栈上分配,而无须改动其他的代码。</p>
<p>从上面的描述我们可以看到并不是所有的对象或者数组都会在堆上分配。由于JIT的存在如果发现某些对象没有逃逸出方法那么就有可能被优化成栈分配。</p>
<p><strong>3.同步消除</strong></p>
<p>如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。</p>
<p>注意这是针对 synchronized 来说的JUC 中的 Lock 并不能被消除。</p>
<p>要开启同步消除,需要加上 -XX:+EliminateLocks 参数。由于这个参数依赖逃逸分析,所以同时要打开 -XX:+DoEscapeAnalysis 选项。</p>
<p>比如下面这段代码JIT 判断对象锁只能被一个线程访问,就可以去掉这个同步的影响。</p>
<pre><code>public class SyncEliminate {
public void test() {
synchronized (new Object()) {
}
}
}
</code></pre>
<p>仓库中也有一个 StringBuffer 和 StringBuilder 的 JMH 测试对比,可以看到在开启了锁消除的情况下,它们的效率相差并不大。</p>
<pre><code>Benchmark Mode Cnt Score Error Units
BuilderVsBufferBenchmark.buffer thrpt 10 90085.927 ± 95174.289 ops/ms
BuilderVsBufferBenchmark.builder thrpt 10 103280.200 ± 76172.538 ops/ms
</code></pre>
<h3>JITWatch</h3>
<p>可以使用 jitwatch 工具来观测 JIT 的一些行为。</p>
<pre><code>https://github.com/AdoptOpenJDK/jitwatch
</code></pre>
<p>在代码的启动参数里加入 LogCompilation 等参数开启记录,将生成一个 jitdemo.log 文件。</p>
<pre><code> -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=jitdemo.log
</code></pre>
<p>使用 jitwatch 工具,可打开这个文件,看到详细的编译结果。</p>
<p><img src="assets/Ciqc1F9Ye36AMYIDAAcn4iSavAY997.png" alt="png" /></p>
<p>下面是一段测试代码:</p>
<pre><code>public class SimpleInliningTest {
public SimpleInliningTest() {
int sum = 0;
// 1_000_000 is F4240 in hex
for (int i = 0; i &lt; 1_000_000; i++) {
sum = this.add(sum, 99);
// 63 hex
}
System.out.println(&quot;Sum:&quot; + sum);
}
public int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
new SimpleInliningTest();
}
}
</code></pre>
<p>从执行后的结果可以看到,热点 for 循环已经使用 JIT 进行了编译,而里面应用的 add 方法,也已经被内联。</p>
<p><img src="assets/CgqCHl9Ye4eAaVmXAATwlN8rh4w940.png" alt="png" /></p>
<h3>小结</h3>
<p>JIT 是现代 JVM 主要的优化点,能够显著地提升程序的执行效率。从解释执行到最高层次的 C2一个数量级的性能提升也是有可能的。但即时编译的过程是非常缓慢的既耗时间也费空间所以这些优化操作会和解释执行同时进行。</p>
<p>值得注意的是JIT 在某些情况下还会出现逆优化。比如一些热部署方式触发的 redefineClass就会造成 JIT 编译结果的失效,相关的内联代码也需要重新生成。</p>
<p>JIT 优化并不见得每次都有用,比如下面这段代码,编译后执行,会发生死循环。但如果你在启动的时候,加上 <strong>-Djava.compiler=NONE</strong> 参数,禁用 JIT它就能够执行下去。</p>
<pre><code>public class Demo {
static final class TestThread extends Thread {
boolean stop = false;
public boolean isStop() {
return stop;
}
@Override
public void run() {
try {
Thread.sleep(100);
} catch (Exception ex) {
ex.printStackTrace();
}
stop = true;
System.out.println(&quot;END&quot;);
}
}
public static void main(String[] args) {
int i = 0;
TestThread test = new TestThread();
test.start();
while(!test.isStop()){
System.out.println(&quot;--&quot;);
i++;
}
}
}
</code></pre>
<p><strong>我们主要看了方法内联、逃逸分析等概念,了解到一些方法在被优化后,对象并不一定是在堆上分配的,它可能在被标量替换后,直接在栈上分配。这几个知识点也是在面试中经常被问到的。</strong></p>
<p>JIT 的这些优化一般都是在后台进程默默地去做了我们不需要关注太多。Code Cache 的容量达到上限,会影响程序执行的效率,但除非你有特别多的代码,默认的 240M 一般来说,足够用了。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Java 性能优化实战-完/17 高级进阶JVM 如何完成垃圾回收?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Java 性能优化实战-完/19 高级进阶JVM 常见优化参数.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997149486f3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>