Java 性能调优概述
为什么要进行性能调优
你希望能够回答如下几类基本问题。 * 如果你们搞了次促销,客户突然暴增十倍,系统有足够的内存来应付这种局面吗? * 客户从应用程序中看到的平均响应时间是多长? * 跟竞争对手比起来怎么样?
要做性能调优,就必须知道并确保实现性能调 优的唯一办法就是性能评测。
1.性能术语
##### 等待时间 > 在给定工作量下处理一个任务单元所消耗的时长。 有价值的新能评测一般都是用一张图形来显示在工作量不断增加的情况下等待时间随之改变的函数关系。 ##### 吞吐量 > 系统在限定资源,限定时长内能完成的单位工作量。用的最多的时在某一参考平台上的每秒事物处理数。 ##### 利用率 > 可用资源中用来处理工作单元的资源百分比。 ##### 效率 > 吞吐量除以所有资源。 ##### 容量 > 任一时刻能通过系统的工作单元数量。 ##### 扩展性 > 当系统得到更多资源时,他的吞吐量或等待时间发生的量。 ##### 退化 > 系统负载增加时出现的退化。
2.性能分析法
要进行真正有效的性能调优,在开始做任何技术类工作之前,应该先认真考虑下面这些问题并找出答案。 * 你正在测量的代码有哪些可观测的环节? * 如何测量那些可观测环节? * 这些可观测环节的目标是什么? * 怎么判断性能调优是否做好了? * 性能调优可接受的最大支出是多少(按开发人员投入的时间和增加的代码复杂度计算)? * 在优化的过程中,哪些东西是你不能舍弃的?
知道在测量什么
做性能调优必须测量一些东西。如果你没有测量可观测环节,就不能算做性能调优。 做性能分析最重要的是知道哪个可观测环节。这就需要我们把测量结果、目标和结论跟一个或多个基本可观测环节结合起来。
常见的可观测项
- 方法handleRequest()运行所需的平均时间。
- 并发客户端数量为10时,系统等待时间的第90个百分位数(第X个百分位数是指将一列数按大小排序,第X位百分位数是指这列数中有X%的数的值比这个数要小。即第90百分位数是指数列中有90%的数小于此数)。
- 把并发用户数从1增长到1000时,响应时间的退化。
知道怎么测量
要精确确定一个代码片段运行需要多长时间就必须通过 * 直接测量,在类源码中插入测量代码 * 在类加载时把类转换成受测类 这两种方法 >JVM工具接口(JVMTI)可以创建非常复杂的分析器,但它需要性能工程师编写本地代码,并且它产生的分析数值本质上是统计平均值,而不是直接测量结果。 ##### 直接测量 直接测量是最容易理解的技术。最简单的是像下面这种形式:
long t0 = System.currentTimeMillis();
someMethonToCall();
long t1 = System.currentTimeMillis();
long elapsed = t1 - t0;
System.out.println("someMethonToCall took " + elapsed + "millis");
但它是侵入式的。需要到处添加这种代码,而且随着测量结果不断增多,代码很容易被数据淹没。除此之外还有其他问题,如果someMethonToCall运行时长不足一毫秒会出现什么情 况?
通过类加载自动测量
把类编译成可执行程序。其中一个关键步骤是在加载字节码时进行转换。这个特性非常强大。其中一个简单的例子就是方法的自动测量。 在这种方法中,特殊的类加载器加载someMethonToCall所属的类时,在方法开始和结束的地方加上记录进入和退出时间的字节码。 这些时间通常会被写入共享的数据结构,由其他线程访问。这些线程一般会将数据写入日志文件,或者通过网络交给负责处理原始数据的服务器。
知道性能目标是什么
清晰的目标能让人注意力集中,所以了解和传达优化的最终目标至关重要。大多数情况下,这个目标简单而明确, 比如: * 将10个并发用户的端到端等待时间的第90个百分位数减少20% * 将handleRequest()的平均等待时间减少40%,方差减少25%
知道什么时候停止优化
理论上来说,知道什么时候停止优化并不难——达成目标之时就是任务完成之日。然而实际上人们很容易陷入性能调优的泥淖。如果事情进展顺利,你肯定想要继续前进并做得更好。 而如果不太顺利,你为了达成目标就会不断尝试新策略。 至于该优化什么,这里有一组非常简单的指导规则: * 首先优化那些重要(通常是调用最频繁),而不是最容易的代码。 * 在遇到那些唾手可得的优化时,把它办了,但要清楚代码的调用频率。 最后再做一轮测量工作。如果还没达成性能目标,你就需要清查一下,看看离命中目标还有 多大差距,以及取得的成绩是不是已经对整体性能产生了你所期望的影响。
知道高性能的成本
所有性能调整都贴着价签 * 分析和优化代码要占用的时间 * 所做的调整可能会引入额外的技术复杂度(也有简化代码的性能优化,但它们不是主流) * 为了让主处理线程运行得更快,可能会引入额外的线程来执行辅助任务,但这些线程可能会在负载较高时对系统整体产生不可预料的影响。
知道过早优化的危险
关于优化,Donald Knuth有段著名的评论: > 程序员浪费了大量时间考虑,或担心程序中无关紧要部分的速度,并且那些尝试改进效率的行为实际上有很强的负面影响……过早优化是万恶之源。
这是要提醒我们: * 没有测量就不能确定程序的关键部分 * 可能不是代码导致等待时间过长——环境中的其他部分也会产生等待时间 * 要进行有意识的、齐心协力的优化 有些优化体现在良好的编码风格上: * 不要分配不需要的对象 * 如果再也不需要调试日志,就去掉它
3.哪里出错了
#### 内存延迟层级 计算机处理器要处理数据。如果它要处理的数据到不了,CPU时钟多快都没用——它只能等 着,执行空操作(NOP). 也就是说在解决延迟时,最重要的两个问题是: * CPU要处理的数据的最近的复本在哪里? * 把它送到核心能用的地方需要多长时间? ##### 寄存器 > 这是CPU上的内存地址,随时可用。这部分内存是指令直接操作的 ##### 主存 > 这一般是DRAM。访问时间在50纳秒左右 ##### 固态磁盘 > 访问这种磁盘所需的时间不足0.1毫秒 ##### 硬盘 > 访问这种磁盘并把数据加载到主存中大概需要5毫秒
为什么Java性能调优存在困难
在JVM或其他任何受控运行时环境上做性能调优天生就比在非受控环境下做调优困难。这是因为C/C++程序员几乎所有事情都要自己做。OS只提供很少的服务,比如基本的线程调度。 在受控系统中,基本观点是不用开发人员自己处理所有细节。这能提高程序员的生产率,但要放弃某些控制权。 造成调优困难的平台特性主要是: * 线程调度 * 垃圾收集(GC) * 即时(JIT)编译 这些特性能以很巧妙的方式交互。 例如,编译子系统用计时器来决定编译哪个方法。也就是 说等待编译的候选方法集可能会受到调度和GC等特性的影响。每次运行时所编译的方法可能都不同。
4.一个来自于硬件的时间问题
##### 硬件时钟 在基于x64的机器里有四种不同的硬件时间源:RTC、8254、TSC以及HPET
麻烦的nanoTime()
Java中有两个获取时间的方法:
System.currentTimeMillis() 和
System.nanoTime()
后面一个用于测量比毫秒更精确的时间,但它可能偏离钟表时间。 这就是说对于间隔较长的时间,nanoTime()基本上是不可信的。只能用它测量较短的时间 间隔,较长(宏观)的时间间隔应该用currentTimeMillis()
时间在性能调优中的作用
- 精确度
- 准确度
- 粒度
- 分布式网络计时
blog comments powered by Disqus