我們對性能調優的大部分討論都是以單機上的系統為中心的。但你應該知道,當涉及網絡上的系統調優時,會有一些特別的問題。網絡上的同步和計時並不容易,而且不僅僅是在互聯網上,即便是以太網也會出現這些問題。
詳細講解分佈式網絡計時超出了本書的範圍,但你應該知道,通常來說,很難得到用於跨越幾台機器的工作流的準確時序。另外,即便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: 868000
Loop1: 801000 nanos ; Loop2: 952000
Loop1: 676000 nanos ; Loop2: 930000
Loop1: 762000 nanos ; Loop2: 869000
Loop1: 706000 nanos ; Loop2: 798000
計時子系統的疑難雜症
結果中的所有納秒值都很整齊,全是一千的整數倍。這表明底層系統調用(
System.nanoTime
最終所調用的)僅僅返回了一個微秒整數值——一微秒是1000納秒。因為這個結果是在Mac筆記本上得到的,所以我們猜測在OS X的底層系統調用只有微秒級的精度,實際上,它調用的是gettimeofday
。
從這個結果來看,doLoop2
所用的時長不是doLoop1
的16倍。這表明內存訪問在總體性能配置中佔有支配性地位。doLoop1
和doLoop2
讀取緩存行的次數相同,而修改數據所用的CPU週期只佔整體時間的一小部分。
我們先來回顧下Java時間系統的要點。
- 大多數系統內部都有幾個不同的時鐘。
- 毫秒計時器是安全可靠的。
- 更高精度的時間需要仔細處理以防止出現偏離。
- 你需要知道計時測量的精確度和準確度。
我們下一個將要討論的是Java平台的垃圾收集子系統。這是性能的決定性因素中非常重要的一部分,並且它有很多可調節的部分,對於做性能分析的開發人員來說都可以成為非常重要的工具。