你寫過超過2500行的方法麼?一般來講,這麼大的方法並很少見,通常都是使用機器輔助生成的爲主,這種狀況在模板編譯或其它語言的自動轉換中比較常見。例如,對一個複雜的JSP頁面,機器有可能會爲它生成一個複雜的servlet方法去實現。java
然而在Hotspot上運行這種大方法,極可能會有性能問題。例如,把文章所附DEMO的play()方法的內容分別重複拷貝一、二、四、八、1六、32次並依次運行,在個人機器(Hotspot_1.6u22/Windows)上獲得的play()的執行消耗時間分別是28.4三、54.7二、106.2八、214.4一、419.30、1476.40毫秒/萬次。在重複拷貝1~16次時,隨着代碼量增長,方法執行所消耗的時間也對應成倍增長。當重複拷貝32次時,方法卻多消耗了80%的時間。若是把這個play()方法拆分紅play1()和play2(),讓它們的方法體都是16次的重複拷貝,play1()最後再調用play2(),那麼,play1()+play2()的執行消耗時間是857.75毫秒/萬次,剛好是以前重複拷貝16次所消耗的時間的兩倍。爲何一樣功能的一段代碼放在一個方法中執行會變慢,拆分紅兩個方法就變快?app
你們知道,JVM一開始是以解釋方式執行字節碼的。當這段代碼被執行的次數足夠多之後,它會被動態優化並編譯成機器碼執行,執行速度會大大加快,這就是所謂的JIT編譯。DEMO的play()方法在被統計消耗時間以前,已經預熱執行了2000次,知足ClientVM的方法JIT編譯閾值CompileThreshold=1500次的要求,那麼,它是否是真的被JIT編譯了呢?咱們能夠增長VM參數」-XX:+PrintCompilation」調查一下。在+PrintCompilation打開之後,列出了JVM在運行時進行過JIT編譯的方法。下面是通過32次重複拷貝的play()方法的JIT編譯記錄(只列出須要關心的部分):less
34 HugeMethodDemo::buildTheWorld (184 bytes) 39 HugeMethodDemo::run (59 bytes)
而分紅兩部分的play1()+plaay2()的JIT編譯記錄則爲:ide
<span style="color: #0000ff"><strong>18 HugeMethodDemo::play1 (4999 bytes) 19 HugeMethodDemo::play2 (4993 bytes)</strong></span> 36 HugeMethodDemo::buildTheWorld (184 bytes) 41 HugeMethodDemo::run (59 bytes)
顯然,通過重複拷貝32次的play()方法沒有通過JIT編譯,始終採用解釋方式執行,而分拆開的play1()+play2()通過JIT編譯,因此難怪play()要慢80%。oop
爲何play()方法不受JVM青睞呢,是太長了麼?這隻能到Hotspot源碼中去翻答案了。在compilationPolicy.cpp中有寫道:post
// Returns true if m is allowed to be compiled bool CompilationPolicy::canBeCompiled(methodHandle m) { if (m->is_abstract()) return false; <span style="color: #0000ff">if (DontCompileHugeMethods && m->code_size() > HugeMethodLimit) return false;</span> // Math intrinsics should never be compiled as this can lead to // monotonicity problems because the interpreter will prefer the // compiled code to the intrinsic version. This can't happen in // production because the invocation counter can't be incremented // but we shouldn't expose the system to this problem in testing // modes. if (!AbstractInterpreter::can_be_compiled(m)) { return false; } return !m->is_not_compilable(); }
當DontCompileHugeMethods=true且代碼長度大於HugeMethodLimit時,方法不會被編譯。DontCompileHugeMethods與HugeMethodLimit的值在globals.hpp中定義:性能
上面兩個參數說明了Hotspot對字節碼超過8000字節的大方法有JIT編譯限制,這就是play()杯具的緣由。因爲使用的是productmode的JRE,咱們只能嘗試關閉DontCompileHugeMethods,即增長VM參數」-XX:-DontCompileHugeMethods」來強迫JVM編譯play()。再次對play()進行測試,耗時855毫秒/萬次,性能終於上來了,輸出的JIT編譯記錄也增長了一行:測試
16 HugeMethodDemo::play (9985 bytes)
使用」-XX:-DontCompileHugeMethods」解除大方法的編譯限制,一個比較明顯的缺點是JVM會嘗試編譯所遇到的全部大方法,者會使JIT編譯任務負擔更重,並且須要佔用更多的CodeCache區域去保存編譯後的代碼。可是優勢是編譯後可讓大方法的執行速度變快,且可能提升GC速度。運行時CodeCache的使用量能夠經過JMX或者JConsole得到,CodeCache的大小在globals.hpp中定義:優化
define_pd_global(intx, ReservedCodeCacheSize, 48*M); product_pd(uintx, InitialCodeCacheSize, "Initial code cache size (in bytes)") product_pd(uintx, ReservedCodeCacheSize, "Reserved code cache size (in bytes) - maximum code cache size") product(uintx, CodeCacheMinimumFreeSpace, 500*K, "When less than X space left, we stop compiling.")
一旦CodeCache滿了,HotSpot會中止全部後續的編譯任務,雖然已編譯的代碼不受影響,可是後面的全部方法都會強制停留在純解釋模式。所以,如非必要,應該儘可能避免生成大方法;若是解除了大方法的編譯限制,則要留意配置CodeCache區的大小,準備更多空間存放編譯後的代碼。ui
最後附上DEMO代碼:
[java] import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; public class HugeMethodDemo { public static void main(String[] args) throws Exception { HugeMethodDemo demo = new HugeMethodDemo(); int warmup = 2000; demo.run(warmup); int loop = 200000; double total = demo.run(loop); double avg = total / loop / 1e6 * 1e4; System.out.println(String.format( "Loop=%d次, " + "avg=%.2f毫秒/萬次", loop, avg)); } private long run(int loop) throws Exception { long total = 0L; for (int i = 0; i < loop; i++) { Map theWorld = buildTheWorld(); StringWriter console = new StringWriter(); long start = System.nanoTime(); play(theWorld, console); long end = System.nanoTime(); total += (end – start); } return total; } private Map buildTheWorld() { Map context = new HashMap(); context.put("name", "D&D"); context.put("version", "1.0"); Map game = new HashMap(); context.put("game", game); Map player = new HashMap(); game.put("player", player); player.put("level", "26"); player.put("name", "jifeng"); player.put("job", "paladin"); player.put("address", "heaven"); player.put("weapon", "sword"); player.put("hp", 150); String[] bag = new String[] { "world_map", "dagger", "magic_1", "potion_1", "postion_2", "key" }; player.put("bag", bag); return context; } private void play(Map theWorld, Writer console) throws Exception { // 重複拷貝的開始位置 if (true) { String name = String.valueOf(theWorld.get("name")); String version = String.valueOf(theWorld.get("version")); console.append("Game ").append(name).append(" (v").append(version).append(")n"); Map game = (Map) theWorld.get("game"); if (game != null) { Map player = (Map) game.get("player"); if (player != null) { String level = String.valueOf(player.get("level")); String job = String.valueOf(player.get("job")); String address = String.valueOf(player.get("address")); String weapon = String.valueOf(player.get("weapon")); String hp = String.valueOf(player.get("hp")); console.append(" You are a ").append(level).append(" level ").append(job) .append(" from ").append(address).append(". n"); console.append(" Currently you have a ").append(weapon).append(" in hand, ") .append("your hp: ").append(hp).append(". n"); console.append(" Here are items in your bag: n"); for (String item : (String[]) player.get("bag")) { console.append(" * ").append(item).append("n"); } } else { console.append("tPlayer not login.n"); } } else { console.append("tGame not start yet.n"); } } // 重複拷貝的結束位置 } } [/java]