第一章:性能与优化定义
* JVM上没有“一键变快”的开关
* 没有小技巧能使Java运行变快
* 没有对程序员隐藏的神秘算法
性能是实验科学
- 定义期待的性能结果
- 测量现有的系统性能
- 决定要优化什么才能达到目标
- 实现它
- 重新测试
- 判断是否得到目标
性能分类
- Throughput-吞吐量
- Latency-延迟:
- Capacity-容量
- Utilization-利用率
- Efficiency-效率
- Scalability-可规模化
- Degradation-倒退化
第二章:JVM简介
解释器与类加载
当打下 java HelloWorld, 操作系统就会启动JVM进程,然后执行HelloWorld中的main方法。
为了能够执行不是这个class文件中的字节码,JVM需要先去加载类。最顶层的类加载器是Bootstrap ClassLoader,它会加载JRE必需的类(Java9以前是rt.jar);
随后创建Extension ClassLoader,这个用得不多,它会加载像Nashorn JavaScript runtime这样的东西。
最后创建的是Application ClassLoader,它会加载classpath中的用户类。
执行字节码
Class文件结构
- Java9字节码开头变成了0xCAFEDADA
速记图
HotSpot简介
第三章:硬件与操作系统
CPU与内存的发展不协调
于是发展出了一层一层的缓存
MESI协议
MESI协议定义了缓存中每一行(一般是64个字节)的四个状态
- Modified:已修改,但还没有写回主内存
- Exclusive:独占,只在此缓存中存在,且与主内存一致
- Shared:共享,可能在其他缓存中也存在,与主内存一致
- Invalid:无效,可能没有被用到,可能马上被丢弃
测试性能代码
一般而言,touchEveryItem写的次数是touchEveryLine的16倍,按理说运行时间也应该是16倍。但是实际情况是,两者情况不会相差很多,这是因为从内存中读取到缓存的次数是差不多的,这才是程序的瓶颈。
public class Caching {
private final int ARR_SIZE = 2 * 1024 * 1024;
private final int[] testData = new int[ARR_SIZE];
private void run() {
System.err.println("Start: "+ System.currentTimeMillis());
for (int i = 0; i < 15_000; i++) {
touchEveryLine();
touchEveryItem();
}
System.err.println("Warmup finished: "+ System.currentTimeMillis());
System.err.println("Item Line");
for (int i = 0; i < 100; i++) {
long t0 = System.nanoTime();
touchEveryLine();
long t1 = System.nanoTime();
touchEveryItem();
long t2 = System.nanoTime();
long elEvery = t1 - t0;
long elLine = t2 - t1;
double diff = elEvery - elLine;
System.err.println(elEvery + " " + elLine +" "+ (100 * diff / elLine));
}
}
private void touchEveryItem() {
for (int i = 0; i < testData.length; i++)
testData[i]++;
}
private void touchEveryLine() {
for (int i = 0; i < testData.length; i += 16)
testData[i]++;
}
public static void main(String[] args) {
Caching c = new Caching();
c.run();
}
}
下图为100次测试结果的运行时间图,真的差不多
现代处理器特性
TLB(Translation Lookaside Buffer): 页表的缓存,完成虚拟地址到物理地址的转换。
Branch Prediction:分支预测,现代处理器有流水线特性(pipeline),分支预测把接下来最有可能执行的分支获取进入pipeline,减少了对指令评估的时间(一般评估要20个时钟周期),当然预测错误的话会降低处理器的效率。
指令重排序:详见JMM规范
操作系统
调度器:调度器会对进程是否能占用CPU资源进行控制,它使用一个run queue来进行调度。
尽管Java规范没有规定Java Thread和系统进程要一一对应,但是“绿色线程”很少被使用。
在线程的时间片运行完成(老式OS为10ms或100ms)OS调度器会将线程放进run queue中,然后线程需要排队。
一个经常被忽略的事实是:CPU运行有效代码的时间会很少很少。
long start = System.currentTimeMillis();
for (int i = 0; i < 1_000; i++) {
Thread.sleep(1);
}
long end = System.currentTimeMillis();
System.out.println("Millis elapsed: " + (end - start));
上述代码按理说会运行1000ms,但是实际上会运行1070ms,咳咳,只能说OS改进了不少。作者说XP上会运行2.8s
上下文切换:这是一个代价很大的操作,在用户线程之间或者在用户模式与内核模式之间切换时发生。在这之间会发生缓存和TLBs因无效被清空,而切换回来的时候又发生了一次清空。
为了减小上下文切换的耗费,Linux提供了vDSO来避免切换到内核模式来完成syscalls.看样子是用户线程直接获取系统的某个数据,而不是切换到内核模式再去获取。
一个简单的系统模型
检测方法
一个表现优秀的应用应该有效的利用系统资源。包括CPU使用率,内存,网络,IO带宽。
第一步是找出资源瓶颈出在什么地方。首先要保证OS本身不是资源消耗大户,因为它应该是用于调度资源的。
CPU使用率是最重要的指标,应用最好能够达到100%的使用率。
vmstate和iostate是两个很重要的工具。其中cs需要好好关注,上下文切换不应该有很多。
- r: running processes
- b: blocking processes
- swpd: 交换区使用空间
- free: 空闲内存
- buff: 用作buffer的内存,作为IO字节的缓冲区
- cache: 用作cache的内存,作为磁盘的缓存区
- si: swap in from disk
- so: swap out to disk
- bi: block in,有多少块(512字节)从IO设备接收过来
- bo: block out,有多少块发送给IO设备
- in: 每秒的打断(interrupt)次数
- cs: 每秒的上下文切换次数
- us: user time
- sy: system time
- id: idle time
- wa: waiting time
- st: stolen time
GC与OS
在HotSpot中,内存是预先分配的由GC来管理,不需要调用OS来分配。因此很少需要上下文切换。
如果OS的CPU使用率过大,说明GC没有花费过多的时间。反之,如果user的CPU使用率过大,GC往往是问题的根源,这时需要去看看GC日志,以及看看新对象的创建频率。
IO
- RDMA:硬件直接将数据加载进用户空间,避免了OS的“double-copy”消耗。
虚拟化
第四章:性能测试模式和反模式
常用的测试类型
- 延迟测试:一次端到端的事务时间是多少?
- 吞吐测试:当前系统可以并发多少事务?
- 负载测试:当前系统能承受特定的负载吗?
- 压力测试:当前系统的临界点是什么?
- 耐久测试:系统跑久了会出现什么反常?
- 计划容量测试:当硬件资源增加时,当前系统的规模是否按预想增加?
- 宕机测试:系统突然挂了会发生什么?
第五章:微基准测试和统计
微基准测试
即小段代码的测试,常用的工具是JMH。
-XX:+PrintCompilation打印编译信息
-verbose:gc打印gc信息
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(1)
public class SortBenchmark {
private static final int N = 1_000;
private static final List<Integer> testData = new ArrayList<>();
@Setup
public static final void setup() {
Random randomGenerator = new Random();
for (int i = 0; i < N; i++) {
testData.add(randomGenerator.nextInt(Integer.MAX_VALUE));
}
System.out.println("Setup Complete");
}
@Benchmark
public List<Integer> classicSort() {
List<Integer> copy = new ArrayList<Integer>(testData);
Collections.sort(copy);
return copy;
}
@Benchmark
public List<Integer> standardSort() {
return testData.stream().sorted().collect(Collectors.toList());
}
@Benchmark
public List<Integer> parallelSort() {
return testData.parallelStream().sorted().collect(Collectors.toList());
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(SortBenchmark.class.getSimpleName()).warmupIterations(100)
.measurementIterations(5).forks(1)
.jvmArgs("-server", "-Xms2048m", "-Xmx2048m")
.addProfiler(GCProfiler.class)
.addProfiler(StackProfiler.class)
.build();
new Runner(opt).run();
}
}
Benchmark Mode Cnt Score Error Units
optjava.SortBenchmark.classicSort thrpt 200 14373.039 ± 111.586 ops/s
optjava.SortBenchmark.parallelSort thrpt 200 7917.702 ± 87.757 ops/s
optjava.SortBenchmark.standardSort thrpt 200 12656.107 ± 84.849 ops/s
Iteration 1:
[GC (Allocation Failure) 52952K->1848K(225792K), 0.0005354 secs]
[GC (Allocation Failure) 52024K->1848K(226816K), 0.0005341 secs]
[GC (Allocation Failure) 51000K->1784K(223744K), 0.0005509 secs]
[GC (Allocation Failure) 49912K->1784K(225280K), 0.0003952 secs]
9526.212 ops/s
Iteration 2:
[GC (Allocation Failure) 49400K->1912K(222720K), 0.0005589 secs]
[GC (Allocation Failure) 49016K->1832K(223744K), 0.0004594 secs]
[GC (Allocation Failure) 48424K->1864K(221696K), 0.0005370 secs]
[GC (Allocation Failure) 47944K->1832K(222720K), 0.0004966 secs]
[GC (Allocation Failure) 47400K->1864K(220672K), 0.0005004 secs]