我一直在微觀基準測試下面的代碼,我注意到一些有趣的事情,我希望有人可以更多地瞭解。它導致了它看起來像for循環可以繼續快速運行,同時阻止JVM中的其他線程。如果那是真的,那麼我想明白爲什麼,如果不是真的,那麼對我可能缺少的東西的任何瞭解將不勝感激。JVM抖動如何由沒有對象分配的for循環引起?
爲了構建這種情況,讓我引導您瞭解我正在運行的基準和結果。
該代碼非常簡單,遍歷數組中的每個元素,總結其內容。重複「targetCount」次數。
public class UncontendedByteArrayReadBM extends Benchmark {
private int arraySize;
private byte[] array;
public UncontendedByteArrayReadBM(int arraySize) {
super("array reads");
this.arraySize = arraySize;
}
@Override
public void setUp() {
super.setUp();
array = new byte[arraySize];
}
@Override
public void tearDown() {
array = null;
}
@Override
public BenchmarkResult invoke(int targetCount) {
long sum = 0;
for (int i=0; i<targetCount; i++) {
for (int j=0; j<arraySize; j++) {
sum += array[j];
}
}
return new BenchmarkResult(((long)targetCount)*arraySize, "uncontended byte array reads", sum);
}
}
在我的4核 - 2Ghz英特爾Sandy Bridged/i7筆記本電腦上,在OSX上運行Java 6(Oracle JVM)。此代碼重複運行到
2.626852686364034無競爭字節數組讀取/ NS [totalTestRun = 3806.837ms]
(I已剔除用於加熱JVM的重複運行)
此結果似乎是合理我。
這裏有趣的是當我開始測量JVM抖動。要做到這一點,我開始一個睡眠1ms的後臺守護程序線程,然後計算出超過1ms的睡眠時間。我改變了報告,爲每次重複測試打印出最大抖動。
2.6109858273078306無競爭字節數組讀取/ NS [maxJitter = 0.411ms totalTestRun = 3829.971ms]
若要在開始實際的測試運行餘監視抖動沒有之前獲得「正常」的抖動爲我的環境,一個想法正在進行的工作和以下讀數是典型的(全部以毫秒爲單位)。因此0.411ms的抖動是正常的,並沒有那麼有趣。
getMaxJitterMillis() = 0.599
getMaxJitterMillis() = 0.37
getMaxJitterMillis() = 0.352
我已經在這個問題的末尾包含了如何測量抖動的代碼。
有趣的一部分。但是,是的它確實在「JVM預熱」期間發生的,因此不是「正常」,但我想更詳細地瞭解如下:
2.4519521584902644 uncontended byte array reads/ns [maxJitter=2561.222ms totalTestRun=4078.383ms]
注意抖動超過2.5秒。通常我會把它放到GC。但是,在測試運行之前,我確實觸發了一個System.gc(),而-XX:+ PrintGCDetails此時不顯示GC。事實上,在任何測試運行期間沒有GC,因爲在總結預先分配的字節的測試中沒有發生對象分配。每次我運行新的測試時都會發生這種情況,因此我並不認爲它是隨機發生的其他進程的干擾。
我的好奇心飆升,因爲當我注意到雖然抖動非常高,但總運行時間以及每納秒的數組元素讀取數量實際上保持不變。所以這裏是一個線程在4核心機器上嚴重滯後的情況,而工作線程本身沒有滯後,並且沒有GC正在進行。
調查進一步我看着什麼熱點編譯器操作的方式,發現通過-XX以下:+ PrintCompilation:
2632 2% com.mosaic.benchmark.datastructures.array.UncontendedByteArrayReadBM::invoke @ 14 (65 bytes)
6709 2% made not entrant com.mosaic.benchmark.datastructures.array.UncontendedByteArrayReadBM::invoke @ -2 (65 bytes)
被打印出這兩條線之間的滯後爲約2.5秒。正確的時候,包含big for循環的方法已將其優化後的代碼標記爲不再進入。
我的理解是,Hotspot在後臺線程上運行,當它準備好交換新代碼時,它會等待已經運行的代碼達到安全點,然後交換。在每個循環體的末尾有一個大的for循環的情況下(可能已經展開了一些循環體)。我不希望有2.5秒的延遲,除非這個換出必須在JVM上執行停止世界事件。它是否在去優化以前的編譯代碼時做到這一點?
所以我對JVM內部專家的第一個問題是,我在這裏的正確軌道上?延遲2.5s是否可以歸因於將該方法標記爲「未進入」。如果是這樣,爲什麼它會對其他線程產生如此極端的影響?如果這不可能是原因,那麼關於還有哪些其他調查方面的想法會很棒。
(爲了完整性,這裏是我使用的測量抖動的代碼)
private static class MeasureJitter extends Thread {
private AtomicLong maxJitterWitnessedNS = new AtomicLong(0);
public MeasureJitter() {
setDaemon(true);
}
public void reset() {
maxJitterWitnessedNS.set(0);
}
public double getMaxJitterMillis() {
return maxJitterWitnessedNS.get()/1000000.0;
}
public void printMaxJitterMillis() {
System.out.println("getMaxJitterMillis() = " + getMaxJitterMillis());
}
@Override
public void run() {
super.run();
long preSleepNS = System.nanoTime();
while(true) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
long wakeupNS = System.nanoTime();
long jitterNS = Math.max(0, wakeupNS - (preSleepNS+1000000));
long max = Math.max(maxJitterWitnessedNS.get(), jitterNS);
maxJitterWitnessedNS.lazySet(max);
preSleepNS = wakeupNS;
}
}
}
同意,這個建議在過去的十五年裏做得很好:)但現在我有一個癢推 我的理解更加困難。我知道在Java中測量時間會面臨相當大的時鐘粒度問題以及延遲,對於System.currentTimeMillis()可能高達16或32ms,這就是爲什麼常見的.4ms抖動沒有達到我的興趣峯值的原因。相比之下,2.5秒是主要的。 – 2013-04-11 10:07:58
我目前認爲上下文切換和外部事件中斷會比我看到的更隨機。 2.5秒的延遲就像發條一樣。 CPU高速緩存級錯失不會顯示更多的數組讀取的吞吐量,而不是其他線程的抖動?除非調度器可能已經將抖動線程分配到運行for循環的同一個內核上(這不會經常產生),並且它尚未將其移動到另一個內核。嗯,如何測試? :) – 2013-04-11 10:11:22
順便說一句如果你有兩個+套接字和一箇舊版本的Windows,你會發現兩個套接字上的CPU正常運行時間是不同的。這可以解釋2.5秒的固定明顯延遲。我建議你也打印出負面的延遲。我預計會在2.5秒延遲之間出現-2.4到-2.5的延遲 – 2013-04-11 10:18:47