<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                本課時我們主要分享一個實踐案例,JIT 參數配置是如何影響程序運行的。 我們在前面的課時中介紹了很多字節碼指令,這也是 Java 能夠跨平臺的保證。程序在運行的時候,這些指令會按照順序解釋執行,但是,這種解釋執行的方式是非常低效的,它需要把字節碼先翻譯成機器碼,才能往下執行。另外,字節碼是 Java 編譯器做的一次初級優化,許多代碼可以滿足語法分析,但還有很大的優化空間。 所以,為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,并進行各種層次的優化。完成這個任務的編譯器,就稱為即時編譯器(Just In Time Compiler),簡稱 JIT 編譯器。 熱點代碼,就是那些被頻繁調用的代碼,比如調用次數很高或者在 for 循環里的那些代碼。這些再次編譯后的機器碼會被緩存起來,以備下次使用,但對于那些執行次數很少的代碼來說,這種編譯動作就純屬浪費。 在第 14 課時我們提到了參數“-XX:ReservedCodeCacheSize”,用來限制 CodeCache 的大小。也就是說,JIT 編譯后的代碼都會放在 CodeCache 里。 如果這個空間不足,JIT 就無法繼續編譯,編譯執行會變成解釋執行,性能會降低一個數量級。同時,JIT 編譯器會一直嘗試去優化代碼,從而造成了 CPU 占用上升。 ![](https://img.kancloud.cn/bb/5b/bb5bada9499ab46d9cb8cc3aa6654b9a_757x277.jpg) #### JITWatch 在開始之前,我們首先介紹一個觀察 JIT 執行過程的圖形化工具:JITWatch,這個工具非常好用,可以解析 JIT 的日志并友好地展示出來。項目地址請點擊這里查看。 下載之后,進入解壓目錄,執行 ant 即可編譯出執行文件。 * [ ] 產生 JIT 日志 我們觀察下面的一段代碼,這段代碼沒有什么意義,而且寫得很爛。在 test 函數中循環 cal 函數 1 千萬次,在 cal 函數中,還有一些冗余的上鎖操作和賦值操作,這些操作在解釋執行的時候,會加重 JVM 的負擔。 ``` public?class?JITDemo?{ ????Integer?a?=?1000; ????public?void?setA(Integer?a)?{ ????????this.a?=?a;????} ????public?Integer?getA()?{ ????????return?this.a; ????} ????public?Integer?cal(int?num)?{ ????????synchronized?(new?Object())?{ ????????????Integer?a?=?getA(); ????????????int?b?=?a?*?10; ????????????b?=?a?*?100; ????????????return?b?+?num; ????????} ????} ????public?int?test()?{ ????????synchronized?(new?Object())?{ ????????????int?total?=?0; ????????????int?count?=?100_000_00; ????????????for?(int?i?=?0;?i?<?count;?i++)?{ ????????????????total?+=?cal(i); ????????????????if?(i?%?1000?==?0)?{ ????????????????????System.out.println(i?*?1000); ????????????????} ????????????} ????????????return?total; ????????} ????} ????public?static?void?main(String[]?args)?{ ????????JITDemo?demo?=?new?JITDemo(); ????????int?total?=?demo.test(); ``` 在方法執行的時候,我們加上一系列參數,用來打印 JIT 最終生成的機器碼,執行命令如下所示: ``` $JAVA_HOME_13/bin/java?-server?-XX:+UnlockDiagnosticVMOptions?-XX:+TraceClassLoading??-XX:+PrintAssembly?-XX:+LogCompilation?-XX:LogFile=jitdemo.log?JITDemo ``` 執行的過程,會輸入到 jitdemo.log 文件里,接下來我們分析這個文件。 #### 使用 ![](https://img.kancloud.cn/2d/6f/2d6f30d7c057554036f6d8ab8c20e3f9_1043x618.jpg) 單擊 open log 按鈕,打開我們生成的日志文件。 ![](https://img.kancloud.cn/62/93/6293f6762c60c8320b9bbe069c8e34cd_1043x618.jpg) 單擊 config 按鈕,加入要分析的源代碼目錄和字節碼目錄。確認后,單擊 start 按鈕進行分析。 在右側找到我們的 test 方法,聚焦光標后,將彈出我們要分析的主要界面。 ![](https://img.kancloud.cn/c4/f5/c4f5713f4c368f998ca6fa77de7174d7_1025x573.jpg) 在同一個界面上,我們能夠看到源代碼、字節碼、機器碼的對應關系。在右上角,還有 C2/OSR/Level4 這樣的字樣,可以單擊切換。 單擊上圖中的 Chain 按鈕,還會彈出一個依賴鏈界面,該界面顯示了哪些方法已經被編譯了、哪些被內聯、哪些是通過普通的方法調用運行的。 ![](https://img.kancloud.cn/a3/27/a3272b863d5259e230edad6009cae449_1025x603.jpg) 使用 JITWatch 可以看到,調用了 1 千萬次的 for 循環代碼,已經被 C2 進行編譯了。 ![](https://img.kancloud.cn/e1/f9/e1f9998d02a4b45071eac7cc3b31c1a3_1026x729.jpg) #### 編譯層次 HotSpot 虛擬機包含多個即時編譯器,有 C1、C2 和 Graal,采用的是分層編譯的模式。使用 jstack 獲得的線程信息,經常能看到它們的身影。 實驗性質的 Graal 可以通過追加 JVM 參數進行開啟,命令行如下: ``` $JAVA_HOME_13/bin/java?-server?-XX:+UnlockDiagnosticVMOptions?-XX:+TraceClassLoading ??-XX:+PrintAssembly?-XX:+LogCompilation?-XX:+UnlockExperimentalVMOptions ???-XX:+UseJVMCICompiler?-XX:LogFile=jitdemo.log?JITDemo ``` 不同層次的編譯器會產生不一樣的效果,機器碼也會不同,我們僅看 C1、C2 的一些特點。 JIT 編譯方式有兩種:一種是編譯方法,另一種是編譯循環。分層編譯將 JVM 的執行狀態分為了五個層次: * 字節碼的解釋執行; * 執行不帶 profiling 的 C1 代碼; * 執行僅帶方法調用次數,以及循環執行次數 profiling 的 C1 代碼; * 執行帶所有 profiling 的 C1 代碼; * 執行 C2 代碼。 其中,profiling 指的是運行時的程序執行狀態數據,比如循環調用的次數、方法調用的次數、分支跳轉次數、類型轉換次數等。JDK 中的 hprof 工具就是一種 profiler。 在不啟用分層編譯的情況下,當方法的調用次數和循環回邊的次數總和,超過由參數 -XX:CompileThreshold 指定的閾值時,便會觸發即時編譯;當啟用分層編譯時,這個參數將會失效,會采用動態調整的方式進行。 常見的優化方法有以下幾種: * 公共子表達式消除 * 數組范圍檢查消除 * 方法內聯 * 逃逸分析 我們重點看一下方法內聯和逃逸分析。 #### 方法內聯 在第 17 課時里,我們可以看到方法調用的開銷是比較大的,尤其是在調用量非常大的情況下。拿簡單的 getter/setter 方法來說,這種方法在 Java 代碼中大量存在,我們在訪問的時候,需要創建相應的棧幀,訪問到需要的字段后,再彈出棧幀,恢復原程序的執行。 如果能夠把這些對象的訪問和操作,納入到目標方法的調用范圍之內,就少了一次方法調用,速度就能得到提升,這就是方法內聯的概念。 C2 編譯器會在解析字節碼的過程中完成方法內聯。內聯后的代碼和調用方法的代碼,會組成新的機器碼,存放在 CodeCache 區域里。 在 JDK 的源碼里,有很多被 @ForceInline 注解的方法,這些方法會在執行的時候被強制進行內聯;而被 @DontInline 注解的方法,則始終不會被內聯,比如下面的一段代碼。 java.lang.ClassLoader 的 getClassLoader 方法將會被強制內聯。 ``` @CallerSensitive ????@ForceInline?//?to?ensure?Reflection.getCallerClass?optimization ????public?ClassLoader?getClassLoader()?{ ????????ClassLoader?cl?=?getClassLoader0(); ????????if?(cl?==?null) ????????????return?null; ????????SecurityManager?sm?=?System.getSecurityManager(); ????????if?(sm?!=?null)?{ ????????????ClassLoader.checkClassLoaderPermission(cl,?Reflection.getCallerClass()); ????????} ????????return?cl; } ``` 方法內聯的過程是非常智能的,內聯后的代碼,會按照一定規則進行再次優化。最終的機器碼,在保證邏輯正確的前提下,可能和我們推理的完全不一樣。在非常小的概率下,JIT 會出現 Bug,這時候可以關閉問題方法的內聯,或者直接關閉 JIT 的優化,保持解釋執行。實際上,這種 Bug 我從來沒碰到過。 ``` -XX:CompileCommand=exclude,com/lagou/Test,test ``` 上面的參數,表示 com.lagou.Test 的 test 方法將不會進行 JIT 編譯,一直解釋執行。 另外,C2 支持的內聯層次不超過 9 層,太高的話,CodeCache 區域會被擠爆,這個閾值可以通過 -XX:MaxInlineLevel 進行調整。相似的,編譯后的代碼超過一定大小也不會再內聯,這個參數由 -XX:InlineSmallCode 進行調整。 有非常多的參數,被用來控制對內聯方法的選擇,整體來說,短小精悍的小方法更容易被優化。 這和我們在日常中的編碼要求是一致的:代碼塊精簡,邏輯清晰的代碼,更容易獲得優化的空間。 ![](https://img.kancloud.cn/1b/a8/1ba8ecb8be406a9fe09be2320c3f171b_1025x573.jpg) 我們使用 JITWatch 再看一下對于 getA() 方法的調用,將鼠標懸浮在字節碼指令上,可以看到方法已經被內聯了。 #### 逃逸分析 逃逸分析(Escape Analysis)是目前 JVM 中比較前沿的優化技術。通過逃逸分析,JVM 能夠分析出一個新的對象使用范圍,從而決定是否要將這個對象分配到堆上。 使用 -XX:+DoEscapeAnalysis 參數可以開啟逃逸分析,逃逸分析現在是 JVM 的默認行為,這個參數可以忽略。 JVM 判斷新創建的對象是否逃逸的依據有: * 對象被賦值給堆中對象的字段和類的靜態變量; * 對象被傳進了不確定的代碼中去運行。 舉個例子,在代碼 1 中,雖然 map 是一個局部變量,但是它通過 return 語句返回,其他外部方法可能會使用它,這就是方法逃逸。另外,如果被其他線程引用或者賦值,則成為線程逃逸。 代碼 2,用完 Map 之后就直接銷毀了,我們就可以說 map 對象沒有逃逸。 代碼1: ``` public?Map?fig(){ ????Map?map?=?new?HashMap(); ????... ????return?map; } ``` 代碼2: ``` public?void?fig(){ ????Map?map?=?new?HashMap(); ????... } ``` 那逃逸分析有什么好處呢? * 同步省略,如果一個對象被發現只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。 * 棧上分配,如果一個對象在子程序中被分配,那么指向該對象的指針永遠不會逃逸,對象有可能會被優化為棧分配。 * 分離對象或標量替換,有的對象可能不需要作為一個連續的內存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內存,而是存儲在 CPU 寄存器中。標量是指無法再分解的數據類型,比如原始數據類型及 reference 類型。 ![](https://img.kancloud.cn/11/77/117785938296d3ae03d1f528cc2f6479_1025x573.jpg) 再來看一下 JITWatch 對 synchronized 代碼塊的分析。根據提示,由于逃逸分析了解到新建的鎖對象 Object 并沒有逃逸出方法 cal,它將會在棧上直接分配。 查看 C2 編譯后的機器碼,發現并沒有同步代碼相關的生成。這是因為 JIT 在分析之后,發現針對 new Object() 這個對象并沒有發生線程競爭的情況,則會把這部分的同步直接給優化掉。我們在代碼層次做了一些無用功,字節碼無法發現它,而 JIT 智能地找到了它并進行了優化。 因此,并不是所有的對象或者數組都會在堆上分配。由于 JIT 的存在,如果發現某些對象沒有逃逸出方法,那么就有可能被優化成棧分配。 #### intrinsic 另外一個不得不提的技術點那就是 intrinsic,這來源于一道面試題:為什么 String 類的 indexOf 方法,比我們使用相同代碼實現的方法,執行效率要高得多? 在翻看 JDK 的源碼時,能夠看到很多地方使用了 HotSpotIntrinsicCandidate 注解。比如 StringBuffer 的 append 方法: ``` ?@Override @HotSpotIntrinsicCandidate public?synchronized?StringBuffer?append(char?c)?{ ????????toStringCache?=?null; ????????super.append(c); ????????return?this; } ``` 被 @HotSpotIntrinsicCandidate 標注的方法,在 HotSpot 中都有一套高效的實現,該高效實現基于 CPU 指令,運行時,HotSpot 維護的高效實現會替代 JDK 的源碼實現,從而獲得更高的效率。 上面的問題中,我們往下跟蹤實現,可以發現 StringLatin1 類中的 indexOf 方法,同樣適用了 HotSpotIntrinsicCandidate 注解,原因也就在于此。 ``` @HotSpotIntrinsicCandidate ????public?static?int?indexOf(byte[]?value,?byte[]?str)?{ ????????if?(str.length?==?0)?{ ????????????return?0; ????????} ????????if?(value.length?==?0)?{ ????????????return?-1; ????????} ????????return?indexOf(value,?value.length,?str,?str.length,?0); ????} ????@HotSpotIntrinsicCandidate ????public?static?int?indexOf(byte[]?value,?int?valueCount,?byte[]?str,?int?strCount,?int?fromIndex)?{ ????????byte?first?=?str[0]; ``` JDK 中這種方法有接近 400 個,可以在 IDEA 中使用 Find Usages 找到它們。 #### 小結 JIT 是現代 JVM 主要的優化點,能夠顯著地增加程序的執行效率,從解釋執行到最高層次的 C2,一個數量級的性能提升也是有可能的。但即時編譯的過程是非常緩慢的,耗時間也費空間,所以這些優化操作會和解釋執行同時進行。 一般,方法首先會被解釋執行,然后被 3 層的 C1 編譯,最后被 4 層的 C2 編譯,這個過程也不是一蹴而就的。 常用的優化手段,有公共子表達式消除、數組范圍檢查消除、方法內聯、逃逸分析等。 其中,方法內聯通過將短小精悍的代碼融入到調用方法的執行邏輯里,來減少方法調用上的開支;逃逸分析通過分析變量的引用范圍,對象可能會使用棧上分配的方式來減少 GC 的壓力,或者使用標量替換來獲取更多的優化。 這個過程的執行細節并不是那么“確定”,在不同的 JVM 中,甚至在不同的 HotSpot 版本中,效果也不盡相同。 使用 JITWatch 工具,能夠看到字節碼和機器碼的對應關系,以及執行過程中的一系列優化操作。若想要了解這個工具的更多功能,可以點擊這里參考 wiki。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看