本文會介紹分層編譯的機制,而後介紹即時編譯器對應用啓動性能的影響。java
本文內容基於HotSpot虛擬機,設計Java版本的地方會在文中說明。web
在引入分層編譯以前,咱們須要手動的選擇編譯器。對於啓動性能有要求的短時運行程序,咱們會選擇C1編譯器,對應參數-client,對於長時間運行的對峯值性能有要求的程序,咱們會選擇C2編譯器,對應參數-server。微信
Java7引入了分層編譯,使用-XX:+TieredCompilation參數開啓,它綜合了C1的啓動性能優點和C2的峯值性能優點。app
在Java8中默認開啓了分層編譯,在Java8中,不管是開啓仍是關閉了分層編譯,-cilent和-server參數都是無效的了。當關閉分層編譯的狀況下,JVM會直接使用C2。dom
分層編譯將JVM中代碼的執行狀態分爲了5個層次,五個層次分別是:oop
(profiling是指在程序執行過程當中收集的程序執行狀態數據,例如在上篇中提到的方法調用次數和循環回邊次數)性能
這幾個層次的代碼執行效率由高到低排序以下:4 > 1 > 2 > 3 > 0測試
其中,1 > 2 > 3的緣由在於profiling越多,其性能開銷也越大。優化
下圖顯示了幾種可能的編譯執行路徑。ui
第一條執行路徑,指的是在一般狀況下,熱點方法會被3層的C1編譯,而後被4層的C2編譯。
第二條執行路徑,指的是字節碼方法較少的狀況下,如getter和setter,此時沒有什麼可收集的profiling,就會在3層編譯後,直接交給1層來編譯。
第三條執行路徑,指的是C1繁忙時,JVM會在解釋執行時收集profiling,而後直接有4層的C2編譯。
第四條執行路徑,指的是C2繁忙時,先由2層的C1編譯再由3層的C1編譯,這樣能夠減小方法在3層的執行時間,最終再交給C2執行。
本小結說明了在開啓分層編譯的狀況下,上述的五個層次的編譯分別在什麼時機觸發。
在上篇的第二小節,介紹了在不開啓分層編譯的狀況下,觸發即時編譯的時機與-XX:CompileThreshold參數有關(具體可參考上篇)。
在開啓分層編譯的狀況下,這個參數設定的閾值將失效,取而代之的是另外一種計算閾值的方案,這個閾值是動態調整的(會乘一個係數s),當方法調用次數和循環回邊次數知足下述兩個公式的任意一個時,將會觸發第X層的即時編譯({X}表示第X層)。
method_invoke_number > Tier{X}InvocationThreshold * s
or
method_invoke_number > Tier{X}MinInvocationThreshold * s 且 method_invoke_number + loop_number > Tier{X}CompileThreshold * s
說明:
* method_invoke_number:方法調用次數
* loop_number:循環回邊次數
* Tier{X}InvocationThreshold:由JMV參數指定,X可取3或4,第3層的默認值爲200,第4層的默認值爲15000
* s:動態調整的係數(接下來會說明它的計算方式)
* Tier{X}MinInvocationThreshold:JVM設定的參數,X可取3或4,第3層的默認值爲100,第4層的默認值爲600
* Tier{X}CompileThreshold:JVM設定的參數,X可取2或3或4,第2層的默認值爲0,第3層的默認值爲2000,第4層的默認值爲15000
複製代碼
PS:在【附加】中提供了查看JVM參數默認值的方式
係數s的計算方式:
s = compiler_method_number_{X} / (Tier{X}LoadFeedback * compiler_thread_number_{X}) + 1
* compiler_method_number_{X}:第X層待編譯方法的數目
* Tier{X}LoadFeedback:JVM參數,X可取3或4,第3層的默認值爲5,第4層的默認值爲3
* compiler_thread_number_{X}:第X層編譯線程數目
複製代碼
compiler_thread_number_{X}的計算方式爲:
在64位的JVM中,默認狀況下編譯線程的總數目thread_total是根據CPU的數量來調整的,thread_total的計算方式以下所示,JVM會把這些線程按照1:2的比例分配給C1和C2。
thread_total = log2(N) * log2(log2(N)) * 3 / 2
* N爲CPU核心數
例如一個4核的機器,總的編譯線程數目thread_total = 3,那麼會給C1分配1個線程,C2分配2個線程
複製代碼
由此能夠計算出,JVM默認配置狀況下,4核CPU,第三層觸發C1即時編譯的閾值爲:
假設第3層有10000個待編譯的方法,係數s = 10000 / (5 * 1) + 1 = 2001
那麼
method_invoke_number > 200 * s = 200 * 2001 = 400200
也就是方法調用次數超過400200次的時候觸發第3層的C1即時編譯。
或者
method_invoke_number > 100 * s = 100 * 2001 = 200100 且 method_invoke_number + loop_number > 2000 * s = 2000 * 2001 = 4002000
即:方法調用次數>200100 而且 方法調用次數+循環回邊次數>4002000次時,觸發3層的C1即時編譯。
同理能夠計算出第4層C2的即時編譯閾值:
method_invoke_number > 30015000時
或者
method_invoke_number > 1200600 且 method_invoke_number + loop_number > 30015000時
會觸發第4層的C2即時編譯。
以上篇的一段代碼爲例,說明分層編譯的日誌。
/** * 添加JVM參數: -XX:+PrintCompilation ,打印編譯日誌 */
public class JITDemo2 {
private static Random random = new Random();
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
int count = 0;
int i = 0;
while (i++ < 15000) {
count += plus();
}
}
// 調用時,編譯器計數器+1
private static int plus() {
int count = 0;
// 每次循環,編譯器計數器+1
for (int i = 0; i < 10; i++) {
count += random.nextInt(10);
}
return random.nextInt(10);
}
}
複製代碼
執行結果以下:
176 1 3 java.util.Arrays::copyOf (19 bytes)
176 6 3 java.io.ExpiringCache::entryFor (57 bytes)
177 7 3 java.util.LinkedHashMap::get (33 bytes)
177 8 2 java.lang.String::hashCode (55 bytes)
177 9 3 java.lang.String::equals (81 bytes)
178 10 2 java.lang.CharacterData::of (120 bytes)
178 11 2 java.lang.CharacterDataLatin1::getProperties (11 bytes)
179 12 3 java.lang.String::<init> (82 bytes)
179 17 n 0 java.lang.System::arraycopy (native) (static)
179 13 2 java.lang.String::indexOf (70 bytes)
179 3 4 java.lang.Object::<init> (1 bytes)
179 2 4 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
179 5 4 java.lang.String::length (6 bytes)
179 15 3 java.lang.Math::min (11 bytes)
180 14 3 java.util.Arrays::copyOfRange (63 bytes)
180 4 4 java.lang.String::charAt (29 bytes)
180 16 3 java.lang.String::indexOf (7 bytes)
180 18 3 java.util.HashMap::hash (20 bytes)
180 19 3 java.lang.String::substring (79 bytes)
181 20 4 java.util.TreeMap::parentOf (13 bytes)
181 21 3 java.lang.Character::toUpperCase (6 bytes)
181 22 3 java.lang.Character::toUpperCase (9 bytes)
181 23 3 java.lang.CharacterDataLatin1::toUpperCase (53 bytes)
181 24 3 java.lang.String::getChars (62 bytes)
182 25 3 java.io.File::isInvalid (47 bytes)
184 26 3 java.lang.String::startsWith (7 bytes)
184 28 3 sun.nio.cs.UTF_8$Encoder::encode (359 bytes)
184 31 s 4 java.lang.StringBuffer::append (13 bytes)
184 32 4 java.lang.AbstractStringBuilder::append (29 bytes)
185 33 4 java.io.WinNTFileSystem::isSlash (18 bytes)
185 29 3 java.lang.String::indexOf (166 bytes)
185 27 3 java.lang.String::startsWith (72 bytes)
186 30 3 java.lang.String::toCharArray (25 bytes)
186 34 3 java.lang.StringBuffer::<init> (6 bytes)
186 35 3 java.lang.AbstractStringBuilder::<init> (12 bytes)
186 37 n 0 sun.misc.Unsafe::getObjectVolatile (native)
186 36 3 java.util.concurrent.ConcurrentHashMap::tabAt (21 bytes)
187 38 n 0 sun.misc.Unsafe::compareAndSwapLong (native)
187 41 3 java.util.Random::nextInt (74 bytes)
187 39 3 java.util.concurrent.atomic.AtomicLong::get (5 bytes)
187 40 3 java.util.concurrent.atomic.AtomicLong::compareAndSet (13 bytes)
187 42 3 java.util.Random::next (47 bytes)
188 43 1 java.util.concurrent.atomic.AtomicLong::get (5 bytes)
188 39 3 java.util.concurrent.atomic.AtomicLong::get (5 bytes) made not entrant
188 44 1 java.util.concurrent.atomic.AtomicLong::compareAndSet (13 bytes)
188 46 4 java.util.Random::nextInt (74 bytes)
188 40 3 java.util.concurrent.atomic.AtomicLong::compareAndSet (13 bytes) made not entrant
* 188 45 3 com.example.demo.gcdemo.JITDemo2::plus (36 bytes)
188 47 4 java.util.Random::next (47 bytes)
189 42 3 java.util.Random::next (47 bytes) made not entrant
* 189 48 4 com.example.demo.gcdemo.JITDemo2::plus (36 bytes)
189 41 3 java.util.Random::nextInt (74 bytes) made not entrant
* 191 45 3 com.example.demo.gcdemo.JITDemo2::plus (36 bytes) made not entrant
複製代碼
說明一下日誌格式(最前面的*號忽略,這是爲了標記出plus方法):
從日誌能夠觀察出,plus方法首先觸發了3層的C1即時編譯,而後觸發了4層的C2的即時編譯,最後被標記爲made not entrant,即plus方法發生了去優化。
這裏爲何會發生去優化呢,筆者猜測,made not entrant也就是不會再被進入,由於即時編譯器會將編譯完的代碼存入CodeCache,而CodeCache是在堆外內存的,JVM進程的結束不會釋放這塊堆外內存,這樣會形成內存泄漏。那麼爲了釋放CodeCache,就須要在JVM結束前對其全部內存進行回收,而CodeCache中的內容被回收的依據是全部線程都退出被標記爲made not entrant方法時,該方法的CodeCache就能夠被回收。
PS:經過下面代碼能夠在程序中獲取CodeCache的使用狀況
// 查看Code Cache使用量
List<MemoryPoolMXBean> beans = ManagementFactory.getMemoryPoolMXBeans();
for (MemoryPoolMXBean bean : beans) {
if ("Code Cache".equalsIgnoreCase(bean.getName())) {
System.out.println("max: " + bean.getUsage().getMax() + " bytes, used: " + bean.getUsage().getUsed() + " bytes");
}
}
複製代碼
先來講一下發現的問題:應用啓動後,CPU使用率和負載飆升,致使部分請求失敗,頻繁報警,大概會持續1分鐘左右。
而後考慮是不是即時編譯器的影響。當時咱們在生產環境使用的是jdk1.7.0_67,且沒有開啓分層編譯,而後想到java8對編譯器作了一些優化,而且是默認開啓分層編譯的,而後將其中的一臺機器升級到java8,再從新啓動,發現CPU使用率和負載都下降了。
因爲當時的截圖沒有了,這裏我本身作了一個web程序的小demo。
下面會分別比較java7環境和java8環境的啓動後CPU使用率和負載變化。
java7默認JVM參數狀況下的CPU使用率和複雜變化(不開啓分層編譯):
CPU使用率:
CPU負載:
Java8默認JVM參數狀況下的CPU使用率和複雜變化(開啓分層編譯):
CPU使用率:
CPU負載:
由此能夠看出,同爲即時編譯器默認參數狀況下,java8在啓動性能上提高了不少。
那如何肯定分層編譯是否會影響啓動性能呢?由於在java7中已經支持了分層編譯,因此在java7環境下將分層編譯打開,就能夠進行比對。
須要說明的是,這個比對並不嚴格,java7在CodeCache的回收上作的很差,這方面在java8中獲得了改進,除此以外還有一些其餘方面的改進,因此這是一個不嚴格的測試,但大致能說明問題。
將java7的啓動參數加上-XX:+TieredCompilation,下面是CPU使用率和CPU負載的變化狀況。
CPU使用率的變化:
CPU負載的變化:
因而可知分層編譯的開啓有利於提高應用的啓動性能。
因爲CodeCache若是越小,GC的次數越頻繁,越影響編譯器的性能,CodeCache過大也很差,會提升單詞GC須要的時間,因此CodeCache儘量要調整成最合適的大小。
PS:CodeCache的GC筆者沒有研究過,因此這裏GC對其的影響也是一個猜想。
查看JVM默認值的方式:
-XX:+PrintFlagsFinal
例如:java -XX:+PrintFlagsFinal -version > options.txt
結果以下:
[1] 極客時間《深刻拆解Java虛擬機》 鄭雨迪