我们对性能调优的大部分讨论都是以单机上的系统为中心的。但你应该知道,当涉及网络上的系统调优时,会有一些特别的问题。网络上的同步和计时并不容易,而且不仅仅是在互联网上,即便是以太网也会出现这些问题。
详细讲解分布式网络计时超出了本书的范围,但你应该知道,通常来说,很难得到用于跨越几台机器的工作流的准确时序。另外,即便NTP这样的标准协议对于高精度工作来说准确度也不够。
在开始讨论垃圾收集之前,我们先看一个前面提到过的例子——缓存对代码性能的影响。
6.4.4 案例研究:理解缓存未命中
对于很多吞吐量较高的代码来说,影响性能的一个主要因素就是一级缓存未命中的数量。
代码清单6-2中的代码操作1MB的数组,并输出执行两个循环中之一所用的时间。在第一个循环中,每隔16个条目对int
数组中的元素加1。一级缓存的一个缓存行中通常有64个字节(在32位JVM上,Java的int是4个字节),所以这意味着每次会读取一个缓存行(64=16*4)。
代码清单6-2 理解缓存未命中
public class CacheTester { private final int ARR_SIZE = 1 * 1024 * 1024; private final int arr = new int[ARR_SIZE]; private void doLoop2 { for (int i=0; i<arr.length; i++) arr[i]++; //处理每个条目 } private void doLoop1 { for (int i=0; i<arr.length; i += 16) arr[i]++;//处理每个缓存行 } private void run { for (int i=0; i<10000; i++) {//代码热身 doLoop1; doLoop2; } for (int i=0; i<100; i++) { long t0 = System.nanoTime; doLoop1; long t1 = System.nanoTime; doLoop2; long t2 = System.nanoTime; long el = t1 - t0; long el2 = t2 - t1; System.out.println("Loop1: "+ el +" nanos ; Loop2: "+ el2); } } public static void main(String args) { CacheTester ct = new CacheTester; ct.run; }}
注意,在你得到准确结果之前应该让代码热热身,以便让JVM对你感兴趣的方法进行编译。我们会在6.6节讨论更多与代码热身相关的内容。
第二个循环,doLoop2
给数组中的每个元素加1,所以看起来它做的工作是doLoop1
的16倍。下面是在笔记本上运行这段代码得到的结果:
Loop1: 634000 nanos ; Loop2: 868000Loop1: 801000 nanos ; Loop2: 952000Loop1: 676000 nanos ; Loop2: 930000Loop1: 762000 nanos ; Loop2: 869000Loop1: 706000 nanos ; Loop2: 798000
计时子系统的疑难杂症
结果中的所有纳秒值都很整齐,全是一千的整数倍。这表明底层系统调用(
System.nanoTime
最终所调用的)仅仅返回了一个微秒整数值——一微秒是1000纳秒。因为这个结果是在Mac笔记本上得到的,所以我们猜测在OS X的底层系统调用只有微秒级的精度,实际上,它调用的是gettimeofday
。
从这个结果来看,doLoop2
所用的时长不是doLoop1
的16倍。这表明内存访问在总体性能配置中占有支配性地位。doLoop1
和doLoop2
读取缓存行的次数相同,而修改数据所用的CPU周期只占整体时间的一小部分。
我们先来回顾下Java时间系统的要点。
- 大多数系统内部都有几个不同的时钟。
- 毫秒计时器是安全可靠的。
- 更高精度的时间需要仔细处理以防止出现偏离。
- 你需要知道计时测量的精确度和准确度。
我们下一个将要讨论的是Java平台的垃圾收集子系统。这是性能的决定性因素中非常重要的一部分,并且它有很多可调节的部分,对于做性能分析的开发人员来说都可以成为非常重要的工具。