《Optimizing Java》读书笔记上

第一章:性能与优化定义

* 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]

HdrHistigram

一个绘制统计图的工具
http://hdrhistogram.github.io/HdrHistogram/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值