mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-15 21:53:49 +08:00
mod
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<audio id="audio" title="第33讲 | 后台服务出现明显“变慢”,谈谈你的诊断思路?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6d/a3/6d15271f8c794b25771478f35e7955a3.mp3"></audio>
|
||||
|
||||
在日常工作中,应用或者系统出现性能问题往往是不可避免的,除了在有一定规模的IT企业或者专注于特定性能领域的企业,可能大多数工程师并不会成为专职的性能工程师,但是掌握基本的性能知识和技能,往往是日常工作的需要,并且也是工程师进阶的必要条件之一,能否定位和解决性能问题也是对你知识、技能和能力的检验。
|
||||
|
||||
今天我要问你的问题是,后台服务出现明显“变慢”,谈谈你的诊断思路?
|
||||
|
||||
## 典型回答
|
||||
|
||||
首先,需要对这个问题进行更加清晰的定义:
|
||||
|
||||
<li>
|
||||
服务是突然变慢还是长时间运行后观察到变慢?类似问题是否重复出现?
|
||||
</li>
|
||||
<li>
|
||||
“慢”的定义是什么,我能够理解是系统对其他方面的请求的反应延时变长吗?
|
||||
</li>
|
||||
|
||||
第二,理清问题的症状,这更便于定位具体的原因,有以下一些思路:
|
||||
|
||||
<li>
|
||||
<p>问题可能来自于Java服务自身,也可能仅仅是受系统里其他服务的影响。初始判断可以先确认是否出现了意外的程序错误,例如检查应用本身的错误日志。<br />
|
||||
对于分布式系统,很多公司都会实现更加系统的日志、性能等监控系统。一些Java诊断工具也可以用于这个诊断,例如通过JFR(<a href="https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/about.htm#JFRUH173">Java Flight Recorder</<!-- [[[read_end]]] -->a>),监控应用是否大量出现了某种类型的异常。<br />
|
||||
如果有,那么异常可能就是个突破点。<br />
|
||||
如果没有,可以先检查系统级别的资源等情况,监控CPU、内存等资源是否被其他进程大量占用,并且这种占用是否不符合系统正常运行状况。</p>
|
||||
</li>
|
||||
<li>
|
||||
监控Java服务自身,例如GC日志里面是否观察到Full GC等恶劣情况出现,或者是否Minor GC在变长等;利用jstat等工具,获取内存使用的统计信息也是个常用手段;利用jstack等工具检查是否出现死锁等。
|
||||
</li>
|
||||
<li>
|
||||
如果还不能确定具体问题,对应用进行Profiling也是个办法,但因为它会对系统产生侵入性,如果不是非常必要,大多数情况下并不建议在生产系统进行。
|
||||
</li>
|
||||
<li>
|
||||
定位了程序错误或者JVM配置的问题后,就可以采取相应的补救措施,然后验证是否解决,否则还需要重复上面部分过程。
|
||||
</li>
|
||||
|
||||
## 考点分析
|
||||
|
||||
今天我选择的是一个常见的并且比较贴近实际应用的的性能相关问题,我提供的回答包括两部分。
|
||||
|
||||
<li>
|
||||
在正面回答之前,先探讨更加精确的问题定义是什么。有时候面试官并没有表达清楚,有必要确认自己的理解正确,然后再深入回答。
|
||||
</li>
|
||||
<li>
|
||||
从系统、应用的不同角度、不同层次,逐步将问题域尽量缩小,隔离出真实原因。具体步骤未必千篇一律,在处理过较多这种问题之后,经验会令你的直觉分外敏感。
|
||||
</li>
|
||||
|
||||
大多数工程师也许并没有全面的性能问题诊断机会,如果被问到也不必过于紧张,你可以向面试官展示诊断问题的思考方式,展现自己的知识和综合运用的能力。接触到一个陌生的问题,通过沟通,能够条理清晰地将排查方案逐步确定下来,也是能力的体现。
|
||||
|
||||
面试官可能会针对某个角度的诊断深入询问,兼顾工作和面试的需求,我会针对下面一些方面进行介绍。目的是让你对性能分析有个整体的印象,在遇到特定领域问题时,即使不知道具体细节的工具和手段,至少也可以找到探索、查询的方向。
|
||||
|
||||
<li>
|
||||
我将介绍业界常见的性能分析方法论。
|
||||
</li>
|
||||
<li>
|
||||
从系统分析到JVM、应用性能分析,把握整体思路和主要工具。对于线程状态、JVM内存使用等很多方面,我在专栏前面已经陆陆续续介绍了很多,今天这一讲也可以看作是聚焦性能角度的一个小结。
|
||||
</li>
|
||||
|
||||
如果你有兴趣进行系统性的学习,我建议参考Charlie Hunt编撰的《Java Performance》或者Scott Oaks的《Java Performance:The Definitive Guide》。另外,如果不希望出现理解偏差,最好是阅读英文版。
|
||||
|
||||
## 知识扩展
|
||||
|
||||
首先,我们来了解一下业界最广泛的性能分析方法论。
|
||||
|
||||
根据系统架构不同,分布式系统和大型单体应用也存在着思路的区别,例如,分布式系统的性能瓶颈可能更加集中。传统意义上的性能调优大多是针对单体应用的调优,专栏的侧重点也是如此,Charlie Hunt曾将其方法论总结为两类:
|
||||
|
||||
<li>
|
||||
自上而下。从应用的顶层,逐步深入到具体的不同模块,或者更近一步的技术细节单元,找到可能的问题和解决办法。这是最常见的性能分析思路,也是大多数工程师的选择。
|
||||
</li>
|
||||
<li>
|
||||
自下而上。从类似CPU这种硬件底层,判断类似[Cache-Miss](https://en.wikipedia.org/wiki/CPU_cache#Cache_miss)之类的问题和调优机会,出发点是指令级别优化。这往往是专业的性能工程师才能掌握的技能,并且需要专业工具配合,大多数是移植到新的平台上,或需要提供极致性能时才会进行。
|
||||
</li>
|
||||
|
||||
例如,将大数据应用移植到SPARC体系结构的硬件上,需要对比和尽量释放性能潜力,但又希望尽量不改源代码。
|
||||
|
||||
我所给出的回答,首先是试图排除功能性错误,然后就是典型的自上而下分析思路。
|
||||
|
||||
第二,我们一起来看看自上而下分析中,各个阶段的常见工具和思路。需要注意的是,具体的工具在不同的操作系统上可能区别非常大。
|
||||
|
||||
**系统性能分析**中,CPU、内存和IO是主要关注项。
|
||||
|
||||
对于CPU,如果是常见的Linux,可以先用top命令查看负载状况,下图是我截取的一个状态。<br />
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/01/3b927b63bec67f99e8dd72860a292601.png" alt="" />
|
||||
|
||||
可以看到,其平均负载(load average)的三个值(分别是1分钟、5分钟、15分钟)非常低,并且暂时看并没有升高迹象。如果这些数值非常高(例如,超过50%、60%),并且短期平均值高于长期平均值,则表明负载很重;如果还有升高的趋势,那么就要非常警惕了。
|
||||
|
||||
进一步的排查有很多思路,例如,我在专栏第18讲曾经问过,怎么找到最耗费CPU的Java线程,简要介绍步骤:
|
||||
|
||||
- 利用top命令获取相应pid,“-H”代表thread模式,你可以配合grep命令更精准定位。
|
||||
|
||||
```
|
||||
top –H
|
||||
|
||||
```
|
||||
|
||||
- 然后转换成为16进制。
|
||||
|
||||
```
|
||||
printf "%x" your_pid
|
||||
|
||||
```
|
||||
|
||||
- 最后利用jstack获取的线程栈,对比相应的ID即可。
|
||||
|
||||
当然,还有更加通用的诊断方向,利用vmstat之类,查看上下文切换的数量,比如下面就是指定时间间隔为1,收集10次。
|
||||
|
||||
```
|
||||
vmstat -1 -10
|
||||
|
||||
```
|
||||
|
||||
输出如下:<br />
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/a0/abd28cb4a771365211e1a01d628213a0.png" alt="" />
|
||||
|
||||
如果每秒上下文(cs,[context switch](https://en.wikipedia.org/wiki/Context_switch))切换很高,并且比系统中断高很多(in,system [](https://en.wikipedia.org/wiki/Interrupt) [interrupt](https://en.wikipedia.org/wiki/Interrupt)),就表明很有可能是因为不合理的多线程调度所导致。当然还需要利用[pidstat](https://linux.die.net/man/1/pidstat)等手段,进行更加具体的定位,我就不再进一步展开了。
|
||||
|
||||
除了CPU,内存和IO是重要的注意事项,比如:
|
||||
|
||||
<li>
|
||||
利用free之类查看内存使用。
|
||||
</li>
|
||||
<li>
|
||||
或者,进一步判断swap使用情况,top命令输出中Virt作为虚拟内存使用量,就是物理内存(Res)和swap求和,所以可以反推swap使用。显然,JVM是不希望发生大量的swap使用的。
|
||||
</li>
|
||||
<li>
|
||||
对于IO问题,既可能发生在磁盘IO,也可能是网络IO。例如,利用iostat等命令有助于判断磁盘的健康状况。我曾经帮助诊断过Java服务部署在国内的某云厂商机器上,其原因就是IO表现较差,拖累了整体性能,解决办法就是申请替换了机器。
|
||||
</li>
|
||||
|
||||
讲到这里,如果你对系统性能非常感兴趣,我建议参考[Brendan Gregg](http://www.brendangregg.com/linuxperf.html)提供的完整图谱,我所介绍的只能算是九牛一毛。但我还是建议尽量结合实际需求,免得迷失在其中。<br />
|
||||
<img src="https://static001.geekbang.org/resource/image/93/40/93aa8c4516fd2266472ca4eab1b0cc40.png" alt="" />
|
||||
|
||||
对于**JVM层面的性能分析**,我们已经介绍过非常多了:
|
||||
|
||||
<li>
|
||||
利用JMC、JConsole等工具进行运行时监控。
|
||||
</li>
|
||||
<li>
|
||||
利用各种工具,在运行时进行堆转储分析,或者获取各种角度的统计数据(如[jstat](https://docs.oracle.com/javase/7/docs/technotes/tools/share/jstat.html) -gcutil分析GC、内存分带等)。
|
||||
</li>
|
||||
<li>
|
||||
GC日志等手段,诊断Full GC、Minor GC,或者引用堆积等。
|
||||
</li>
|
||||
|
||||
这里并不存在放之四海而皆准的办法,具体问题可能非常不同,还要看你是否能否充分利用这些工具,从种种迹象之中,逐步判断出问题所在。
|
||||
|
||||
对于**应用**[**Profiling**](https://en.wikipedia.org/wiki/Profiling_(computer_programming)),简单来说就是利用一些侵入性的手段,收集程序运行时的细节,以定位性能问题瓶颈。所谓的细节,就是例如内存的使用情况、最频繁调用的方法是什么,或者上下文切换的情况等。
|
||||
|
||||
我在前面给出的典型回答里提到,一般不建议生产系统进行Profiling,大多数是在性能测试阶段进行。但是,当生产系统确实存在这种需求时,也不是没有选择。我建议使用JFR配合[JMC](http://www.oracle.com/technetwork/java/javaseproducts/mission-control/java-mission-control-1998576.html)来做Profiling,因为它是从Hotspot JVM内部收集底层信息,并经过了大量优化,性能开销非常低,通常是低于 **2%** 的;并且如此强大的工具,也已经被Oracle开源出来!
|
||||
|
||||
所以,JFR/JMC完全具备了生产系统Profiling的能力,目前也确实在真正大规模部署的云产品上使用过相关技术,快速地定位了问题。
|
||||
|
||||
它的使用也非常方便,你不需要重新启动系统或者提前增加配置。例如,你可以在运行时启动JFR记录,并将这段时间的信息写入文件:
|
||||
|
||||
```
|
||||
Jcmd <pid> JFR.start duration=120s filename=myrecording.jfr
|
||||
|
||||
```
|
||||
|
||||
然后,使用JMC打开“.jfr文件”就可以进行分析了,方法、异常、线程、IO等应有尽有,其功能非常强大。如果你想了解更多细节,可以参考相关[指南](https://blog.takipi.com/oracle-java-mission-control-the-ultimate-guide/)。
|
||||
|
||||
今天我从一个典型性能问题出发,从症状表现到具体的系统分析、JVM分析,系统性地整理了常见性能分析的思路;并且在知识扩展部分,从方法论和实际操作的角度,让你将理论和实际结合,相信一定可以对你有所帮助。
|
||||
|
||||
## 一课一练
|
||||
|
||||
关于今天我们讨论的题目你做到心中有数了吗? 今天的思考题是,Profiling工具获取数据的主要方式有哪些?各有什么优缺点。
|
||||
|
||||
请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。
|
||||
|
||||
你的朋友是不是也在准备面试呢?你可以“请朋友读”,把今天的题目分享给好友,或许你能帮到他。
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
<audio id="audio" title="第34讲 | 有人说“Lambda能让Java程序慢30倍”,你怎么看?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2a/3b/2a43ff7d3bad7e7bbe91e6e6df965d3b.mp3"></audio>
|
||||
|
||||
在上一讲中,我介绍了Java性能问题分析的一些基本思路。但在实际工作中,我们不能仅仅等待性能出现问题再去试图解决,而是需要定量的、可对比的方法,去评估Java应用性能,来判断其是否能够符合业务支撑目标。今天这一讲,我会介绍从Java开发者角度,如何从代码级别判断应用的性能表现,重点理解最广泛使用的基准测试(Benchmark)。
|
||||
|
||||
今天我要问你的问题是,有人说“Lambda能让Java程序慢30倍”,你怎么看?
|
||||
|
||||
为了让你清楚地了解这个背景,请参考下面的代码片段。在实际运行中,基于Lambda/Stream的版本(lambdaMaxInteger),比传统的for-each版本(forEachLoopMaxInteger)慢很多。
|
||||
|
||||
```
|
||||
// 一个大的ArrayList,内部是随机的整形数据
|
||||
volatile List<Integer> integers = …
|
||||
|
||||
// 基准测试1
|
||||
public int forEachLoopMaxInteger() {
|
||||
int max = Integer.MIN_VALUE;
|
||||
for (Integer n : integers) {
|
||||
max = Integer.max(max, n);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
// 基准测试2
|
||||
public int lambdaMaxInteger() {
|
||||
return integers.stream().reduce(Integer.MIN_VALUE, (a, b) -> Integer.max(a, b));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 典型回答
|
||||
|
||||
我认为,“Lambda能让Java程序慢30倍”这个争论实际反映了几个方面:
|
||||
|
||||
第一,基准测试是一个非常有效的通用手段,让我们以直观、量化的方式,判断程序在特定条件下的性能表现。
|
||||
|
||||
第二,基准测试必须明确定义自身的范围和目标,否则很有可能产生误导的结果。前面代码片段本身的逻辑就有瑕疵,更多的开销是源于自动装箱、拆箱(auto-boxing/unboxing),而不是源自Lambda和Stream,所以得出的初始结论是没有说服力的。
|
||||
|
||||
第三,虽然Lambda/Stream为Java提供了强大的函数式编程能力,但是也需要正视其局限性:
|
||||
|
||||
<li>
|
||||
一般来说,我们可以认为Lambda/Stream提供了与传统方式接近对等的性能,但是如果对于性能非常敏感,就不能完全忽视它在特定场景的性能差异了,例如:**初始化的开销**。 Lambda并不算是语法糖,而是一种新的工作机制,在首次调用时,JVM需要为其构建[CallSite](https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/CallSite.html)实例。这意味着,如果Java应用启动过程引入了很多Lambda语句,会导致启动过程变慢。其实现特点决定了JVM对它的优化可能与传统方式存在差异。
|
||||
</li>
|
||||
<li>
|
||||
增加了程序诊断等方面的复杂性,程序栈要复杂很多,Fluent风格本身也不算是对于调试非常友好的结构,并且在可检查异常的处理方面也存在着局限性等。
|
||||
</li>
|
||||
|
||||
## 考点分析
|
||||
|
||||
今天的题目是源自于一篇有争议的[文章](https://blog.takipi.com/benchmark-how-java-8-lambdas-and-streams-can-make-your-code-5-times-slower/),原文后来更正为“如果Stream使用不当,会让你的代码慢5倍”。针对这个问题我给出的回答,并没有纠结于所谓的“快”与“慢”,而是从工程实践的角度指出了基准测试本身存在的问题,以及Lambda自身的局限性。
|
||||
|
||||
从知识点的角度,这个问题考察了我在[专栏第7讲](http://time.geekbang.org/column/article/7514)中介绍过的自动装箱/拆箱机制对性能的影响,并且考察了Java 8中引入的Lambda特性的相关知识。除了这些知识点,面试官还可能更加深入探讨如何用基准测试之类的方法,将含糊的观点变成可验证的结论。
|
||||
|
||||
对于Java语言的很多特性,经常有很多似是而非的 “秘籍”,我们有必要去伪存真,以定量、定性的方式探究真相,探讨更加易于推广的实践。找到结论的能力,比结论本身更重要,因此在今天这一讲中,我们来探讨一下:
|
||||
|
||||
<li>
|
||||
基准测试的基础要素,以及如何利用主流框架构建简单的基准测试。
|
||||
</li>
|
||||
<li>
|
||||
进一步分析,针对保证基准测试的有效性,如何避免偏离测试目的,如何保证基准测试的正确性。
|
||||
</li>
|
||||
|
||||
## 知识扩展
|
||||
|
||||
首先,我们先来整体了解一下基准测试的主要目的和特征,专栏里我就不重复那些[书面的定义](https://baike.baidu.com/item/%E5%9F%BA%E5%87%86%E6%B5%8B%E8%AF%95)了。
|
||||
|
||||
性能往往是特定情景下的评价,泛泛地说性能“好”或者“快”,往往是具有误导性的。通过引入基准测试,我们可以定义性能对比的明确条件、具体的指标,进而保证得到**定量的、可重复的**对比数据,这是工程中的实际需要。
|
||||
|
||||
不同的基准测试其具体内容和范围也存在很大的不同。如果是专业的性能工程师,更加熟悉的可能是类似[SPEC](https://www.spec.org/)提供的工业标准的系统级测试;而对于大多数Java开发者,更熟悉的则是范围相对较小、关注点更加细节的微基准测试(Micro-Benchmark)。我在文章开头提的问题,就是典型的微基准测试,也是我今天的侧重点。
|
||||
|
||||
**什么时候需要开发微基准测试呢?**
|
||||
|
||||
我认为,当需要对一个大型软件的某小部分的性能进行评估时,就可以考虑微基准测试。换句话说,微基准测试大多是API级别的验证,或者与其他简单用例场景的对比,例如:
|
||||
|
||||
<li>
|
||||
你在开发共享类库,为其他模块提供某种服务的API等。
|
||||
</li>
|
||||
<li>
|
||||
你的API对于性能,如延迟、吞吐量有着严格的要求,例如,实现了定制的HTTP客户端API,需要明确它对HTTP服务器进行大量GET请求时的吞吐能力,或者需要对比其他API,保证至少对等甚至更高的性能标准。
|
||||
</li>
|
||||
|
||||
所以微基准测试更是偏基础、底层平台开发者的需求,当然,也是那些追求极致性能的前沿工程师的最爱。
|
||||
|
||||
**如何构建自己的微基准测试,选择什么样的框架比较好?**
|
||||
|
||||
目前应用最为广泛的框架之一就是[JMH](http://openjdk.java.net/projects/code-tools/jmh/),OpenJDK自身也大量地使用JMH进行性能对比,如果你是做Java API级别的性能对比,JMH往往是你的首选。
|
||||
|
||||
JMH是由Hotspot JVM团队专家开发的,除了支持完整的基准测试过程,包括预热、运行、统计和报告等,还支持Java和其他JVM语言。更重要的是,它针对Hotspot JVM提供了各种特性,以保证基准测试的正确性,整体准确性大大优于其他框架,并且,JMH还提供了用近乎白盒的方式进行Profiling等工作的能力。
|
||||
|
||||
使用JMH也非常简单,你可以直接将其依赖加入Maven工程,如下图:<br />
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/68/0dd290f8842959cb02d6c3a434a58e68.png" alt="" />
|
||||
|
||||
也可以,利用类似下面的命令,直接生成一个Maven项目。
|
||||
|
||||
```
|
||||
$ mvn archetype:generate \
|
||||
-DinteractiveMode=false \
|
||||
-DarchetypeGroupId=org.openjdk.jmh \
|
||||
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
|
||||
-DgroupId=org.sample \
|
||||
-DartifactId=test \
|
||||
-Dversion=1.0
|
||||
|
||||
```
|
||||
|
||||
JMH利用注解(Annotation),定义具体的测试方法,以及基准测试的详细配置。例如,至少要加上“@Benchmark”以标识它是个基准测试方法,而BenchmarkMode则指定了基准测试模式,例如下面例子指定了吞吐量(Throughput)模式,还可以根据需要指定平均时间(AverageTime)等其他模式。
|
||||
|
||||
```
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testMethod() {
|
||||
// Put your benchmark code here.
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
当我们实现了具体的测试后,就可以利用下面的Maven命令构建。
|
||||
|
||||
```
|
||||
mvn clean install
|
||||
|
||||
```
|
||||
|
||||
运行基准测试则与运行不同的Java应用没有明显区别。
|
||||
|
||||
```
|
||||
java -jar target/benchmarks.jar
|
||||
|
||||
```
|
||||
|
||||
更加具体的上手步骤,请参考相关[指南](http://www.baeldung.com/java-microbenchmark-harness)。JMH处处透着浓浓的工程师味道,并没有纠结于完善的文档,而是提供了非常棒的[样例代码](http://hg.openjdk.java.net/code-tools/jmh/file/3769055ad883/jmh-samples/src/main/java/org/openjdk/jmh/samples),所以你需要习惯于直接从代码中学习。
|
||||
|
||||
**如何保证微基准测试的正确性,有哪些坑需要规避?**
|
||||
|
||||
首先,构建微基准测试,需要从白盒层面理解代码,尤其是具体的性能开销,不管是CPU还是内存分配。这有两个方面的考虑,第一,需要保证我们写出的基准测试符合测试目的,确实验证的是我们要覆盖的功能点,这一讲的问题就是个典型例子;第二,通常对于微基准测试,我们通常希望代码片段确实是有限的,例如,执行时间如果需要很多毫秒(ms),甚至是秒级,那么这个有效性就要存疑了,也不便于诊断问题所在。
|
||||
|
||||
更加重要的是,由于微基准测试基本上都是体量较小的API层面测试,最大的威胁来自于过度“聪明”的JVM!Brain Goetz曾经很早就指出了微基准测试中的[典型问题](https://www.ibm.com/developerworks/java/library/j-jtp02225/)。
|
||||
|
||||
由于我们执行的是非常有限的代码片段,必须要保证JVM优化过程不影响原始测试目的,下面几个方面需要重点关注:
|
||||
|
||||
<li>保证代码经过了足够并且合适的预热。我在[专栏第1讲](http://time.geekbang.org/column/article/6845)中提到过,默认情况,在server模式下,JIT会在一段代码执行10000次后,将其编译为本地代码,client模式则是1500次以后。我们需要排除代码执行初期的噪音,保证真正采样到的统计数据符合其稳定运行状态。<br />
|
||||
通常建议使用下面的参数来判断预热工作到底是经过了多久。</li>
|
||||
|
||||
```
|
||||
-XX:+PrintCompilation
|
||||
|
||||
```
|
||||
|
||||
我这里建议考虑另外加上一个参数,否则JVM将默认开启后台编译,也就是在其他线程进行,可能导致输出的信息有些混淆。
|
||||
|
||||
```
|
||||
-Xbatch
|
||||
|
||||
```
|
||||
|
||||
与此同时,也要保证预热阶段的代码路径和采集阶段的代码路径是一致的,并且可以观察PrintCompilation输出是否在后期运行中仍然有零星的编译语句出现。
|
||||
|
||||
- 防止JVM进行无效代码消除(Dead Code Elimination),例如下面的代码片段中,由于我们并没有使用计算结果mul,那么JVM就可能直接判断无效代码,根本就不执行它。
|
||||
|
||||
```
|
||||
public void testMethod() {
|
||||
int left = 10;
|
||||
int right = 100;
|
||||
int mul = left * right;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如果你发现代码统计数据发生了数量级程度上的提高,需要警惕是否出现了无效代码消除的问题。
|
||||
|
||||
解决办法也很直接,尽量保证方法有返回值,而不是void方法,或者使用JMH提供的[BlackHole](http://hg.openjdk.java.net/code-tools/jmh/file/3769055ad883/jmh-core/src/main/java/org/openjdk/jmh/infra/Blackhole.java)设施,在方法中添加下面语句。
|
||||
|
||||
```
|
||||
public void testMethod(Blackhole blackhole) {
|
||||
// …
|
||||
blackhole.consume(mul);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- 防止发生常量折叠(Constant Folding)。JVM如果发现计算过程是依赖于常量或者事实上的常量,就可能会直接计算其结果,所以基准测试并不能真实反映代码执行的性能。JMH提供了State机制来解决这个问题,将本地变量修改为State对象信息,请参考下面示例。
|
||||
|
||||
```
|
||||
@State(Scope.Thread)
|
||||
public static class MyState {
|
||||
public int left = 10;
|
||||
public int right = 100;
|
||||
}
|
||||
|
||||
public void testMethod(MyState state, Blackhole blackhole) {
|
||||
int left = state.left;
|
||||
int right = state.right;
|
||||
int mul = left * right;
|
||||
blackhole.consume(mul);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<li>
|
||||
另外JMH还会对State对象进行额外的处理,以尽量消除伪共享([False Sharing](https://blogs.oracle.com/dave/java-contended-annotation-to-help-reduce-false-sharing))的影响,标记@State,JMH会自动进行补齐。
|
||||
</li>
|
||||
<li>
|
||||
如果你希望确定方法内联(Inlining)对性能的影响,可以考虑打开下面的选项。
|
||||
</li>
|
||||
|
||||
```
|
||||
-XX:+PrintInlining
|
||||
|
||||
```
|
||||
|
||||
从上面的总结,可以看出来微基准测试是一个需要高度了解Java、JVM底层机制的技术,是个非常好的深入理解程序背后效果的工具,但是也反映了我们需要审慎对待微基准测试,不被可能的假象蒙蔽。
|
||||
|
||||
我今天介绍的内容是相对常见并易于把握的,对于微基准测试,GC等基层机制同样会影响其统计数据。我在前面提到,微基准测试通常希望执行时间和内存分配速率都控制在有限范围内,而在这个过程中发生GC,很可能导致数据出现偏差,所以Serial GC是个值得考虑的选项。另外,JDK 11引入了[Epsilon GC](http://openjdk.java.net/jeps/318),可以考虑使用这种什么也不做的GC方式,从最大可能性去排除相关影响。
|
||||
|
||||
今天我从一个争议性的程序开始,探讨了如何从开发者角度而不是性能工程师角度,利用(微)基准测试验证你在性能上的判断,并且介绍了其基础构建方式和需要重点规避的风险点。
|
||||
|
||||
## 一课一练
|
||||
|
||||
关于今天我们讨论的题目你做到心中有数了吗?我们在项目中需要评估系统的容量,以计划和保证其业务支撑能力,谈谈你的思路是怎么样的?常用手段有哪些?
|
||||
|
||||
请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。
|
||||
|
||||
你的朋友是不是也在准备面试呢?你可以“请朋友读”,把今天的题目分享给好友,或许你能帮到他。
|
||||
|
||||
|
||||
195
极客时间专栏/Java核心技术面试精讲/模块四 Java性能基础/第35讲 | JVM优化Java代码时都做了什么?.md
Normal file
195
极客时间专栏/Java核心技术面试精讲/模块四 Java性能基础/第35讲 | JVM优化Java代码时都做了什么?.md
Normal file
@@ -0,0 +1,195 @@
|
||||
<audio id="audio" title="第35讲 | JVM优化Java代码时都做了什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d7/c0/d78aafda6e5f89e06e930bee2bd4ffc0.mp3"></audio>
|
||||
|
||||
我在专栏上一讲介绍了微基准测试和相关的注意事项,其核心就是避免JVM运行中对Java代码的优化导致失真。所以,系统地理解Java代码运行过程,有利于在实践中进行更进一步的调优。
|
||||
|
||||
今天我要问你的问题是,JVM优化Java代码时都做了什么?
|
||||
|
||||
与以往我来给出典型回答的方式不同,今天我邀请了隔壁专栏[《深入拆解Java虚拟机》](http://time.geekbang.org/column/intro/108?utm_source=app&utm_medium=article&utm_campaign=108-presell&utm_content=java)的作者,同样是来自Oracle的郑雨迪博士,让他以JVM专家的身份去思考并回答这个问题。
|
||||
|
||||
## 来自JVM专栏作者郑雨迪博士的回答
|
||||
|
||||
JVM在对代码执行的优化可分为运行时(runtime)优化和即时编译器(JIT)优化。运行时优化主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配机制(如TLAB)等。除此之外,还有一些专门用于优化解释执行效率的,比如说模版解释器、内联缓存(inline cache,用于优化虚方法调用的动态绑定)。
|
||||
|
||||
JVM的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。它采用了多种优化方式,包括静态编译器可以使用的如方法内联、逃逸分析,也包括基于程序运行profile的投机性优化(speculative/optimistic optimization)。这个怎么理解呢?比如我有一条instanceof指令,在编译之前的执行过程中,测试对象的类一直是同一个,那么即时编译器可以假设编译之后的执行过程中还会是这一个类,并且根据这个类直接返回instanceof的结果。如果出现了其他类,那么就抛弃这段编译后的机器码,并且切换回解释执行。
|
||||
|
||||
当然,JVM的优化方式仅仅作用在运行应用代码的时候。如果应用代码本身阻塞了,比如说并发时等待另一线程的结果,这就不在JVM的优化范畴啦。
|
||||
|
||||
## 考点分析
|
||||
|
||||
感谢郑雨迪博士从JVM的角度给出的回答。今天这道面试题在专栏里有不少同学问我,也是会在面试时被面试官刨根问底的一个知识点,郑博士的回答已经非常全面和深入啦。
|
||||
|
||||
大多数Java工程师并不是JVM工程师,知识点总归是要落地的,面试官很有可能会从实践的角度探讨,例如,如何在生产实践中,与JIT等JVM模块进行交互,落实到如何真正进行实际调优。
|
||||
|
||||
在今天这一讲,我会从Java工程师日常的角度出发,侧重于:
|
||||
|
||||
<li>
|
||||
从整体去了解Java代码编译、执行的过程,目的是对基本机制和流程有个直观的认识,以保证能够理解调优选择背后的逻辑。
|
||||
</li>
|
||||
<li>
|
||||
从生产系统调优的角度,谈谈将JIT的知识落实到实际工作中的可能思路。这里包括两部分:如何收集JIT相关的信息,以及具体的调优手段。
|
||||
</li>
|
||||
|
||||
## 知识扩展
|
||||
|
||||
首先,我们从整体的角度来看看Java代码的整个生命周期,你可以参考我提供的示意图。<br />
|
||||
<img src="https://static001.geekbang.org/resource/image/12/9d/12526a857a7685af0d7c2ee389c0ca9d.png" alt="" />
|
||||
|
||||
我在[专栏第1讲](http://time.geekbang.org/column/article/6845)就已经提到过,Java通过引入字节码这种中间表达方式,屏蔽了不同硬件的差异,由JVM负责完成从字节码到机器码的转化。
|
||||
|
||||
通常所说的编译期,是指javac等编译器或者相关API等将源码转换成为字节码的过程,这个阶段也会进行少量类似常量折叠之类的优化,只要利用反编译工具,就可以直接查看细节。
|
||||
|
||||
javac优化与JVM内部优化也存在关联,毕竟它负责了字节码的生成。例如,Java 9中的字符串拼接,会被javac替换成对StringConcatFactory的调用,进而为JVM进行字符串拼接优化提供了统一的入口。在实际场景中,还可以通过不同的[策略](http://openjdk.java.net/jeps/280)选项来干预这个过程。
|
||||
|
||||
今天我要讲的重点是**JVM运行时的优化**,在通常情况下,编译器和解释器是共同起作用的,具体流程可以参考下面的示意图。<br />
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/78/5c095075dcda0f39f0e7395ab9636378.png" alt="" />
|
||||
|
||||
JVM会根据统计信息,动态决定什么方法被编译,什么方法解释执行,即使是已经编译过的代码,也可能在不同的运行阶段不再是热点,JVM有必要将这种代码从Code Cache中移除出去,毕竟其大小是有限的。
|
||||
|
||||
就如郑博士所回答的,解释器和编译器也会进行一些通用优化,例如:
|
||||
|
||||
<li>
|
||||
锁优化,你可以参考我在[专栏第16讲](http://time.geekbang.org/column/article/9042)提供的解释器运行时的源码分析。
|
||||
</li>
|
||||
<li>
|
||||
Intrinsic机制,或者叫作内建方法,就是针对特别重要的基础方法,JDK团队直接提供定制的实现,利用汇编或者编译器的中间表达方式编写,然后JVM会直接在运行时进行替换。
|
||||
</li>
|
||||
|
||||
这么做的理由有很多,例如,不同体系结构的CPU在指令等层面存在着差异,定制才能充分发挥出硬件的能力。我们日常使用的典型字符串操作、数组拷贝等基础方法,Hotspot都提供了内建实现。
|
||||
|
||||
而**即时编译器(JIT)**,则是更多优化工作的承担者。JIT对Java编译的基本单元是整个方法,通过对方法调用的计数统计,甄别出热点方法,编译为本地代码。另外一个优化场景,则是最针对所谓热点循环代码,利用通常说的栈上替换技术(OSR,On-Stack Replacement,更加细节请参考[R大的文章](https://github.com/AdoptOpenJDK/jitwatch/wiki/Understanding-the-On-Stack-Replacement-(OSR)-optimisation-in-the-HotSpot-C1-compiler)),如果方法本身的调用频度还不够编译标准,但是内部有大的循环之类,则还是会有进一步优化的价值。
|
||||
|
||||
从理论上来看,JIT可以看作就是基于两个计数器实现,方法计数器和回边计数器提供给JVM统计数据,以定位到热点代码。实际中的JIT机制要复杂得多,郑博士提到了[逃逸分析](https://en.wikipedia.org/wiki/Escape_analysis)、[循环展开](https://en.wikipedia.org/wiki/Loop_unrolling)、方法内联等,包括前面提到的Intrinsic等通用机制同样会在JIT阶段发生。
|
||||
|
||||
第二,有哪些手段可以探查这些优化的具体发生情况呢?
|
||||
|
||||
专栏中已经陆陆续续介绍了一些,我来简单总结一下并补充部分细节。
|
||||
|
||||
- 打印编译发生的细节。
|
||||
|
||||
```
|
||||
-XX:+PrintCompilation
|
||||
|
||||
```
|
||||
|
||||
- 输出更多编译的细节。
|
||||
|
||||
```
|
||||
-XX:UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=<your_file_path>
|
||||
|
||||
```
|
||||
|
||||
JVM会生成一个xml形式的文件,另外, LogFile选项是可选的,不指定则会输出到
|
||||
|
||||
```
|
||||
hotspot_pid<pid>.log
|
||||
|
||||
```
|
||||
|
||||
具体格式可以参考Ben Evans提供的[JitWatch](https://github.com/AdoptOpenJDK/jitwatch/)工具和[分析指南](http://www.oracle.com/technetwork/articles/java/architect-evans-pt1-2266278.html)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/07/46/07b00499b0ca857fc3ccd51f7046d946.png" alt="" />
|
||||
|
||||
- 打印内联的发生,可利用下面的诊断选项,也需要明确解锁。
|
||||
|
||||
```
|
||||
-XX:+PrintInlining
|
||||
|
||||
```
|
||||
|
||||
- 如何知晓Code Cache的使用状态呢?
|
||||
|
||||
很多工具都已经提供了具体的统计信息,比如,JMC、JConsole之类,我也介绍过使用NMT监控其使用。
|
||||
|
||||
第三,我们作为应用开发者,有哪些可以触手可及的调优角度和手段呢?
|
||||
|
||||
- 调整热点代码门限值
|
||||
|
||||
我曾经介绍过JIT的默认门限,server模式默认10000次,client是1500次。门限大小也存在着调优的可能,可以使用下面的参数调整;与此同时,该参数还可以变相起到降低预热时间的作用。
|
||||
|
||||
```
|
||||
-XX:CompileThreshold=N
|
||||
|
||||
```
|
||||
|
||||
很多人可能会产生疑问,既然是热点,不是早晚会达到门限次数吗?这个还真未必,因为JVM会周期性的对计数的数值进行衰减操作,导致调用计数器永远不能达到门限值,除了可以利用CompileThreshold适当调整大小,还有一个办法就是关闭计数器衰减。
|
||||
|
||||
```
|
||||
-XX:-UseCounterDecay
|
||||
|
||||
```
|
||||
|
||||
如果你是利用debug版本的JDK,还可以利用下面的参数进行试验,但是生产版本是不支持这个选项的。
|
||||
|
||||
```
|
||||
-XX:CounterHalfLifeTime
|
||||
|
||||
```
|
||||
|
||||
- 调整Code Cache大小
|
||||
|
||||
我们知道JIT编译的代码是存储在Code Cache中的,需要注意的是Code Cache是存在大小限制的,而且不会动态调整。这意味着,如果Code Cache太小,可能只有一小部分代码可以被JIT编译,其他的代码则没有选择,只能解释执行。所以,一个潜在的调优点就是调整其大小限制。
|
||||
|
||||
```
|
||||
-XX:ReservedCodeCacheSize=<SIZE>
|
||||
|
||||
```
|
||||
|
||||
当然,也可以调整其初始大小。
|
||||
|
||||
```
|
||||
-XX:InitialCodeCacheSize=<SIZE>
|
||||
|
||||
```
|
||||
|
||||
注意,在相对较新版本的Java中,由于分层编译(Tiered-Compilation)的存在,Code Cache的空间需求大大增加,其本身默认大小也被提高了。
|
||||
|
||||
- 调整编译器线程数,或者选择适当的编译器模式
|
||||
|
||||
JVM的编译器线程数目与我们选择的模式有关,选择client模式默认只有一个编译线程,而server模式则默认是两个,如果是当前最普遍的分层编译模式,则会根据CPU内核数目计算C1和C2的数值,你可以通过下面的参数指定的编译线程数。
|
||||
|
||||
```
|
||||
-XX:CICompilerCount=N
|
||||
|
||||
```
|
||||
|
||||
在强劲的多处理器环境中,增大编译线程数,可能更加充分的利用CPU资源,让预热等过程更加快速;但是,反之也可能导致编译线程争抢过多资源,尤其是当系统非常繁忙时。例如,系统部署了多个Java应用实例的时候,那么减小编译线程数目,则是可以考虑的。
|
||||
|
||||
生产实践中,也有人推荐在服务器上关闭分层编译,直接使用server编译器,虽然会导致稍慢的预热速度,但是可能在特定工作负载上会有微小的吞吐量提高。
|
||||
|
||||
- 其他一些相对边界比较混淆的所谓“优化”
|
||||
|
||||
比如,减少进入安全点。严格说,它远远不只是发生在动态编译的时候,GC阶段发生的更加频繁,你可以利用下面选项诊断安全点的影响。
|
||||
|
||||
```
|
||||
-XX:+PrintSafepointStatistics ‑XX:+PrintGCApplicationStoppedTime
|
||||
|
||||
```
|
||||
|
||||
注意,在JDK 9之后,PrintGCApplicationStoppedTime已经被移除了,你需要使用“-Xlog:safepoint”之类方式来指定。
|
||||
|
||||
很多优化阶段都可能和安全点相关,例如:
|
||||
|
||||
<li>
|
||||
在JIT过程中,逆优化等场景会需要插入安全点。
|
||||
</li>
|
||||
<li>
|
||||
常规的锁优化阶段也可能发生,比如,偏斜锁的设计目的是为了避免无竞争时的同步开销,但是当真的发生竞争时,撤销偏斜锁会触发安全点,是很重的操作。所以,在并发场景中偏斜锁的价值其实是被质疑的,经常会明确建议关闭偏斜锁。
|
||||
</li>
|
||||
|
||||
```
|
||||
-XX:-UseBiasedLocking
|
||||
|
||||
```
|
||||
|
||||
主要的优化手段就介绍到这里,这些方法都是普通Java开发者就可以利用的。如果你想对JVM优化手段有更深入的了解,建议你订阅JVM专家郑雨迪博士的专栏。
|
||||
|
||||
## 一课一练
|
||||
|
||||
关于今天我们讨论的题目你做到心中有数了吗? 请思考一个问题,如何程序化验证final关键字是否会影响性能?
|
||||
|
||||
请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。
|
||||
|
||||
你的朋友是不是也在准备面试呢?你可以“请朋友读”,把今天的题目分享给好友,或许你能帮到他。
|
||||
|
||||
点击下方图片进入JVM专栏<br />
|
||||
[<img src="https://static001.geekbang.org/resource/image/2a/d5/2a62e58cbdf56a5dc40748567d346fd5.jpg" alt="" />](http://time.geekbang.org/column/intro/108?utm_source=app&utm_medium=article&utm_campaign=108-presell&utm_content=java)
|
||||
Reference in New Issue
Block a user