Java性能权威指南学习笔记-1

Java性能权威指南学习笔记-1

导论

JVM调优标志

布尔标志

-XX:+FlagName 表示开启 -XX:-FlagName 表示关闭

附带参数的标志

-XX:FlagName=something,表示将标志flagName的值设置为something

Client和Server类虚拟机

Java的自动优化前提是机器被分为”Client”和”Server”。

Microsoft Windows上运行的任何32位JVM(无论机器上CPU的个数是多少),以及单CPU机器(不论是什么操作系统)上运行的任何32位JVM,都是client类机器。所有其他机器(包括所有64位JVM)都被认为是Server类。

常见的优化

借助性能分析来优化代码,重点关注性能分析中最耗时的操作

利用奥卡姆剃刀原则诊断性能问题

为应用中最常用的操作编写简单算法。

性能测试方法

原则1:测试真实应用

微基准测试

  1.必须使用被测的结果

  2.不要包括无关的操作

  3.必须输入合理的参数

宏基准测试

介基准测试

  快速小结

  1.好的微基准测试既难写,价值又有限。如果你必须使用它,那可以用它来快速了解性能,但不要依赖它们。

  测试完整应用是了解它实际运行的唯一途径。

  在模块或者操作基本隔离性能——介基准测试——相对于全应用测试来说,是一种合理的折中途径,而不是替代方法。

原则2:理解批处理流逝时间、吞吐量和响应时间

批处理流逝时间

吞吐量测试

响应时间测试

负载生成器

快速小结

1.Java性能测试中很少使用面向批处理的测试(或者任何没有热身期的测试),但这种测试可以产生何忧价值的结果。

2.其他可以测量吞吐量或响应时间的测试,则依赖负载是否以固定的速率加载(也就是说,给予模拟的客户端思考时间)。

原则3:用统计方法应对性能的变化

1.正确判定测试结果间的差异需要统计分析,通过统计分析才能确定这些差异是不是归因于随机因素

2.可以用严谨的t检验来比较测试结果,实现上述目的

3.t检验可以告知我们变动存在的概率,却无法高数我们哪种变动该忽略,而哪种该追查。如何在两者之间找到平衡,是性能调优工程的艺术魅力所在。

原则4:尽早频繁测试

自动化一切

测试一切

在真实系统上运行

快速小结

1.虽然频繁的性能测试很重要,但并非毫无代价,在日常开发周期中需要仔细斟酌。

2.自动化测试系统可以收集所有机器和程序的全部统计数据,这可以为查找性能衰减问题提供必不可少的线索。

Java性能调优工具箱

操作系统的工具和分析

CPU使用率

CPU运行队列

快速小结

1.检查应用性能时,首先应该审查CPU时间

2.优化代码的目的是提升而不是降低(更短时间内的)CPU使用率

3.在试图深入优化应用前,应该先弄清楚为何CPU使用率低。

磁盘使用率

快速小结

1.对于所有应用来说,监控磁盘使用率非常重要。即便不直接写磁盘的应用,系统交换仍然会影响他们的性能。

2.写入磁盘的应用遇到瓶颈,是因为写入数据的效率不高(吞吐量太低),或者是因为写入太多数据(吞吐量太高)。

网络使用率

快速小结

1.对于网络的应用来说,务必要监控网络以确保它不是瓶颈。

2.往网络写数据的应用遇到瓶颈,可能是因为写数据的效率太低(吞吐量太低),也可能是因为写入了太多的数据(吞吐量太高)

Java监控工具

JDK自带工具

jcmd

它用来打印Java进程所涉及的基本类、线程和VM信息。

% jcmd process_id command optinal_arguments

jcmd help 可以列出所有的命令。jcmd help 可以给出特定命令的语法。

jconsole

提供JVM活动的图形化视图,包括线程的使用、类的使用和GC活动。

jhat

读取内存堆转储,并有助于分析。这是事后使用的工具。

jmap

提供堆转储和其他JVM内存使用的信息。可以适用于脚本,但堆转储必须在事后分析工具中使用,

jinfo

查看JVM的系统属性,可以动态设置一些系统属性。可适用于脚本。

jstack

转储Java进程的栈信息。可适用于脚本。

jvisualvm

监视JVM的GUI工具,可用来剖析运行的应用,分析JVM堆转储(事后活动,虽然jvisualvm也可以试试抓取程序的堆转储)

基本的VM信息

运行时长

% jcmd process_id VM.uptime

系统属性

%jcmd _id VM.system_properties

%jinfo -sysprop process_id

这包括通过命令行-D标志设置的所有属性,应用动态添加的所有属性和JVM默认属性

JVM版本

%jcmd process_id VM.version

JVM命令行

jconsole的“VM摘要”页可以显示程序所用的命令行,或者用jcmd显示

%jcmd process_id VM.command_line

JVM调优标志

可以用一下方式获得对应生效的JVM调优标志

%jcmd process_id VM.flag [-all]

调优标志

想知道特定平台所设置的标志是什么,可以执行以下命令: %java other_options -XX:+PrintFlagsFinal -version name:=value 表示标志使用的是非默认值。可能原因: (1)标志值直接在命令行指定 (2)其他标志间接改变了该标志的值。 (3)JVM自动优化计算出来的默认值。 最后一列的含义: (1)product 表示在所有平台上的默认设置都是一致的 (2)pd product表示标志的默认值是独立于平台的 (3)manageable 运行时可以动态更改标志的值 (4)C2 diagnostic 为编译器工程师提供诊断输出,帮助理解编译器正以什么方式运作

获取进程中所有标志的值

 %jinfo -flags process_id

 jinfo带有-flags时可以提供所有标志的信息,否则只打印命令行所指定的标志。

jinfo检查单个标志的值

 %jinfo -flag PrintGCDetails process_id

jinfo修改manageable的标志的值

 %jinfo -flag -PrintGCDetails process_id #turn off PrintGCDetails

快速小结

1.jcmd可用来查找运行中的应用所在JVM的基本信息——包括所有调优标志的值

2.命令行上添加-XX:+Printflagsfinal 可输出标志的默认值

3.jinfo在检查(某些情况下可以更改)单个标志时很有用

线程信息

jconsole和jvisualvm可以实时显示应用中运行的线程的数量

%jstack process_id 显示了每个线程的栈的众多输出

%jcmd process_id Thread.print 显示了每个线程栈的众多输出

类信息

jconsole或jstat可以提供应用已使用类的个数。jstat还能提供类编译相关的信息

实时GC分析

jconsole可以用实时图显示堆得使用情况

jcmd可以执行GC操作

jmap可以打印堆得概况、永久代信息或者创建堆转储。

jstat可以为垃圾收集器正在执行的操作生成许多视图。

事后堆转储

jvisualvm的GUI界面可以捕获堆转储,也可以用命令行jcmd或jmap生成。

堆转储是堆使用情况的快照,可以用不同的工具进行分析,包括jvisualvm和jhat。

性能分析工具

采样分析器

性能分析的两种模式

数据采样

数据探查

快速小结

1.采样分析器是最常用的分析器

2.因为采样分析器的采样频率相对较低,所以引入的测量失真也较小。

3.不同的采样分析器各有千秋,针对不同应用各有所长。

探查分析器

探查分析器相比于采样分析器,侵入性更强,但它们可以给出关于程序内部所发生的更有价值的信息

探查分析器会在类加载时更改类的字节码(即插入统计调用次数的代码)。相比采样分析器,探查分析器更可能会将性能偏差引入应用。

快速小结

1.探查分析器可以给出更多的应用信息,但相对采样分析器,它对应用的影响更大。

2.探查分析器应该仅在小代码区域——一些类和包——中设置使用,以限制对应用性能的影响。

阻塞方法和线程时间线

快速小结

1.线程被阻塞可能是性能问题,也可能不是,有必要进一步调查它们为何被阻塞。

2.通过被阻塞的方法调用,或者分析线程的时间线,可以辨认出被阻塞的线程

本地分析器

本地分析器是指分析JVM自身的工具。这类工具可以看到JVM内部的工作原理,如果应用自身含有本地库,这类工具也能看到本地库代码的内部。

快速小结

1.本地性能分析器可以提供JVM和应用代码内部的信息。

2.如果本地性能分析器显示GC占用了主要的CPU时间,优化垃圾收集器就是正确的做法。然而,如果显示编译线程占用了明显的时间,则说明通常对应用性能没什么影响。

Java任务控制

Java Mission Control

JMC的程序(jmc)开启了一个窗口以显示当前机器上的JVM进程,你可以选择一个或多个进行监控,

Java飞行记录器

JMC的关键特性是Java飞行记录器(Java Flight Recorder,JFR)。JFR数据是JVM的历史事件,这些可以用来诊断JVM的历史性能和操作。 JFR的基本操作是开启一组事件,每当选择的事件发生时,就会保存相应的数据(保存在内存或者文件中)。数据流保存在循环缓冲中,所以只有最近的事件。

开启JFR

在Oracle JVM的商业版本中,JFR初始为关闭。为了开启它,可以在应用的启动命令行上添加标志-XX:+UnlockCommercialFeatures -XX:+flightRecorder。这会开启JFR特性,但直到记录过程自身开始时才会记录信息

1.通过JMC开启JFR

2.通过命令行开启JFR

控制记录应该在何时以及如何发生

 JVM用-XX:+flightRecorderOptions=string 参数方式启东市,可以控制这些记录参数。 参数中的string是一列逗号分隔的名字-值对。

  name=name 用以标识记录的名字

  defaultrecording=表示初始时是否开启记录。默认为false。对于响应性分析,应该设为true

  setting=path JFR设置文件的文件名

  delay=time 记录开始前延迟的时间量

  duration=time 记录持续的时间

  filename=path 记录文件名

  compress= 记录是否开启压缩(gzip)。默认为false

  maxage=time 循环缓冲中保留记录的最长时间

  maxsize=size 记录循环缓冲的最大尺寸

 所有选项可在程序运行时(假设-XX:+flightRecorder已先指定),用jcmd来控制

  开启飞行记录 %jcmd process_id JFR.start [options_list] option_list是一组用逗号分隔的名字-值对,控制记录如何进行。选项和使用命令行时的标志完全一致。

 如果开始持续记录,可以在任何时候,通过以下命令将当前循环缓冲里的数据转储到文件中: %jcmd process_id JFR.dump [options_list]

  name=name 在这个名字下的记录已经开始

  recording=n JFR记录的编号

  filename=path 转储文件的位置

 对于给定的进程,可能开启了多个JFR记录,以下命令可以查看开启的记录: %jcmd process_id JFR.check [verbose]

 进程放弃记录的命令: %jcmd process_id JFR.stop [options_list]

  name=name 停止记录的名字

  recording=n 停止记录的编号(可由JFR.check获得)

  discard=boolean 如果为true,则丢弃数据而不是写到前面所提供的文件中(如果有的话)

  filename=path 数据写到给定的路径上

选择JFR事件

当前的JFR支持77种事件,大多数是周期性事件:它们的周期以毫秒记, 其他事件仅当事件的持续时间超出阈值是才会触发。

JFR捕获的事件(包括事件和阈值)都定义在模板中(可以通过命令行的设置选项选择)。JFR自带了两个模块

默认模板:限制了事件使得开销效率1%

性能分析模板:大多数基于阈值的事件被设置为每10毫秒触发

快速小结

1.由于JFR内建于JVM,所以可以最大可能性地查看JVM内部

2.像其他工具一样,JFR给应用引入了一些开销。对于日常使用,可以开启JFR,以较低的开销收集大量的信息

3.JFR用于性能分析,但它在生产系统中也很有用,所以你可以检查那些导致失败的事件。

JIT编译器

概览

因为Java程序运行的是理想化的二进制代码,所以它能在代码执行时将其编译成平台特定的二进制代码。 由于这个编译是在程序执行时进行的,因此被称为“即时编译”(JIT)

快速小结

1.Java的设计结合了脚本语言的平台独立性和编译型语言的本地性能

2.Java文件被编译成中间语言(Java字节码),然后在运行时被JVM进一步编译成汇编语言

3.字节码编译成汇编语言的过程中有大量的优化极大地改善了性能

调优入门

编译器标志

-client -server -d64

分层编译开启形式-XX:+TieredCompilation

client编译器(C1)开启编译比server(C2)编译器要早

优化启动

快速启动常用client编译器

快速小结

分层编译的启动时间可以非常接近于client编译器的启动时间

优化批处理

快速小结

1.对计算量固定的任务来说,应该选择实际执行任务最快的编译器

2.分层编译是批处理任务合理的默认选择

优化长时间运行的应用

快速小结

1.对于长时间运行的应用,应该一直使用server编译器,最好配合分层编译

Java和JIT编译器版本

JIT编译器3种版本

32位client编译器(-client)

32位server编译器(-server)

64位server编译器(-d64)

快速小结

1.不同的Java支持不同的编译器

2.不同的操作系统和架构支持的编译器也不同

3.程序不必指定编译器,而是仰仗平台所支持的编译器

编译器中级调优

调优代码缓存

-XX:ReservedCodeCacheSize=N (对特定编译器来说,N为默认的值)标志可以设置代码缓存的最大值。代码缓存 的管理和大多数JVM内存一样,有初始值(由-XX:IniticalCodeCacheSize=N指定)。

代码缓存设为1GB,JVM就会保留1GB的本地内存空间。虽然这部分内存在需要时才会分配,但它仍然是被保留的,这意味着为了满足保留内存,你的机器必须有足够的虚拟内存。

通过jconsoleMemory(内存)面板的Memory Pool Code Cache图表,可以监控代码缓存

快速小结

1.代码缓存是一种有最大值的资源,它会影响JVM可运行的编译代码总量

2.分层编译很容易达到代码缓存默认配置的上限(特别是在Java7中)。使用分层编译时,应该监控代码缓存,必要时应该增加它的大小。

编译阈值

编译时基于两种JVM计数器的:方法调用计数器和方法中的循环回边计数器。回边实际上可看作是循环完成执行的次数,所谓循环完成执行,包括达到循环自身的末尾,也包括执行了像continue这样的分支语句。

标准编译

JVM执行某个Java方法时,会检查该方法的两种计数器总数,然后判定该方法是否适合编译。如果适合,该方法就进入编译队列。

由-XX:CompileThreshold=N标志触发。使用client编译器时,N的默认值是1500,使用server编译器时为10000.这个标志的阈值等于回边计数器加上方法调用计数器的总和

栈上替换(On-Stack Replacement,OSR)

如果循环真的很长,循环每完成一轮,回边计数器就会增加并被检测。如果循环的回边计数器超过阈值,那么这个循环(不是整个方法)就可以被编译。

OSR编译由3个标志触发: OSR trigger=(CompileThreshold*((OnstackReplacePercentage-InterpreterProfilePercentage)/100)) 所有编译器中的-XX:InterpreterProfilePercentage=N标志的默认值为33。client编译器-XX:OnStackReplacePercentage=N的默认值为933,所以在它开始OSR编译前,回边计数器需要达到13500。在server编译器中,由于OnStackReplacePercentage默认值为140,所以当回边计数器达到10700时才开始OSR编译。

每种计数器的值都会周期性减少(特别是当JVM达到安全点时)。实际上,计数器只是方法或循环最新热度的度量。

快速小结

1.当方法和循环执行次数达到某个阈值的时候,就会发生编译。

2.改变阈值会导致代码提早或推后编译

3.由于计数器会随着时间而减少,以至于“温热”的方法可能永远都打不到编译的阈值(特别是对server编译器来说)

检测编译过程

-XX:+PrintCompilation(默认为false)

如果开启PrintCompilation,每次编译一个方法(或循环)时,JVM就会打印一行被编译的内容信息。

用jstat检测编译

编译日志需要在程序启动时开启-XX:+PrintCompilation。如果程序启动时没有开启这个标志,可以用jstat了解编译器内部的部分工作情况

jstat有两个有关编译器信息的标志。

 -compiler标志提供了关于多少方法被编译的概要信息 % jstat -compiler pid(线程id)

 可以用-printcompilation标志获取最新被编译的方法。jstat借助一个可选参数反复执行操作,你可以看到随时间变化有哪些方法被编译了。 %jstat -printcompilation 5003 1000 每1000毫秒输出一次进程ID为5003的信息

快速小结

1.观察代码如何被编译的最好方法是开启PrintCompilation

2.PrintCompilation开启后所输出的信息可以用来确认编译是否和预期一样

高级编译器调优

编译线程

编译队列并不严格遵守先进先出的原则:调用次数多的方法有更高的优先级。

当使用client编译器时,JVM会开启一个编译线程;使用server编译器时,则会开启两个这样的线程。当启用分层编译时,JVM默认开始多个client和server线程,线程数依据一个略复杂的等式而定,包括目标平台CPU取双对数之后的数值。

编译器的线程数(3种编译器都是如此)可通过-XX:CICompilerCount=N标志来设置。这是JVM处理队列的线程总数;对分层编译来说,其中三分之一(至少一个)将用来处理client编译器队列,其余的线程(至少一个)用来处理server编译器队列。

-XX:+BackgroundCompilation标志,默认值为true。标志着编译队列的处理是异步执行的。如果设置为false,当一个方法适合编译,执行该方法的代码将一直等到它确实被编译之后才执行(而不是继续在解释器中执行)。用-Xbatch可以禁止后台编译

快速小结

1.放置在编译队列中的方法的编译会被异步执行。

2.队列病不是严格按照先后顺序的,队列中的热点方法会在其他方法之前编译。这是编译输出日志的ID为乱序的另一个原因

内联

编译器所做的最重要的优化是方法内联。

内联默认是开启的。可通过-XX:-Inline关闭。

如果从源代码编译JVM,那可以用-XX:+PrintInlining生成带调试信息的版本。这个参数会提供所有关于编译器如何进行内联决策的信息。

方法是否内联取决于它有多热以及它的大小。JVM依据内部计算来判定方法是否是热点方法(譬如,调用很频繁);是否是热点并不直接与任何调优参数相关。如果方法因调用频繁而可以内联,那只有在它的字节码小于325字节时(或-XX:MaxFreqInlineSize=N所设定的任意值)才会内联。否则,只有方法很小时,即小于35字节(或-XX:MaxInlineSize=N所设定的任意值)时才会内联。

快速小结

1.内联是编译器所能做的最有利的优化,特别是堆属性封装良好的面向对象的代码来说。

2.几乎用不着调节内联参数,且提倡这样做的建议往往忽略了常规内联和频繁调用内联直接的关系。当考察内联效应时,确保考虑这两种情况。

逃逸分析

如果开启逃逸分析(-XX:+DoEscapeAnalysis,默认为true),server编译器将会执行一些非常激进的优化措施。

快速小结

1.逃逸分析是编译器所能做的最复杂的优化,此类优化常常会导致微基准测试失败

2.逃逸分析常常会给不正确的同步代码引入"bug"

逆优化

逆优化意味着编译器不得不“撤销”之前的某些编译;结果是应用的性能降低——至少是直到编译器重新编译相应代码为止

有两种逆优化的情形:代码状态分别为”made not entrant”(代码被丢弃)和”made zombie”(产生僵尸代码)时

代码被丢弃

有两种原因导致代码被丢弃。可能是和类与接口的工作方式有关,也可能与分层编译的实现细节有关。

第二种导致代码被丢弃的原因是分层编译。在分层编译中,代码先由client编译器编译,然后由server编译器编译。当server编译器编译好代码后,JVM必须替换client编译器所编译的代码。它会将老代码标记为废弃,也用同样的办法替换新编译(也更有效)的代码。因此,当程序使用分层编译时,编译日志就会显示许多倍丢弃的方法。

逆优化僵尸代码

1.逆优化使得编译器可以回到之前版本的编译代码

2.先前的优化不再有效时,才会发生代码逆优化

3.代码逆优化时,会对性能产生一些小而短暂的影响,不过新编译的代码会尽快地再次热身

4.分层编译时,如果代码之前由client编译器编译而现在由server编译器优化,就会发生逆优化。

分层编译级别

程序使用分层编译时,编译日志中会输出代码所编译的分层级别。

编译级别

0:解释代码

1:简单C1编译代码

2:受限的C1编译代码

3:完全C1编译代码

4:C2编译代码

典型的编译日志可以显示,多数方法第一次编译的级别是3,即完全C1编译。(当然,所有方法都从级别0开始)如果方法运行得足够频繁,它就会编译成级别4(级别3的代码就会被丢弃)。最常见的情况是:client编译器从获取了代码如何使用的信息进行优化时才开始编译。

快速小结

1.分层编译可以在2种编译器和5种编译级别之间进行

2.不建议人为更改级别

小结

从调优角度看,简单的选择就是对所有应用都使用server编译器和分层编译,这将解决90%的与编译器相关的性能问题

(1)不用担心小方法——特别是getter和setter,因为它们很容易内联。

(2)需要编译的代码在编译队列中。队列中代码越多,程序达到最佳性能的时间越久。

(3)虽然代码缓存的大小可以(也应该)调整,但它仍然是有限的资源

(4)代码越简单,优化越多。分析反馈和逃逸分析可以使代码更快,但复杂的循环结构和大方法限制了它的有效性。

下一篇

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片