1. 程式人生 > >Java效能優化指南系列(三):理解JIT編譯器

Java效能優化指南系列(三):理解JIT編譯器

即時編譯器概述

  • 編譯器在編譯過程中通常會考慮很多因素。比如:彙編指令的順序。假設我們要將兩個暫存器的值進行相加,執行這個操作一般只需要一個CPU週期;但是在相加之前需要將資料從記憶體讀到暫存器中,這個操作是需要多個CPU週期的。編譯器一般可以做到,先啟動資料載入操作,然後執行其它指令,等資料載入完成後,再執行相加操作。由於直譯器在解釋執行的過程中,每次只能看到一行程式碼,所以很難生成上述這樣的高效指令序列。而編譯器可以事先看到所有程式碼,因此,一般來說,解釋性程式碼比編譯性程式碼要慢。不過,解釋性程式碼具有可移植性的優勢。
  • Java的實現在解釋性和編譯性之間進行了折中。Java程式碼是編譯性的,它會被編譯成一個平臺獨立的位元組碼程式。
    JVM負責載入、解釋、執行這些位元組碼程式,在這個過程中,還可能會將這些位元組碼實時編譯成目標機器碼,以便提升效能。
  • 在本章中,我們主要關注JVM是如何解釋、執行、編譯位元組碼的。

編譯熱點程式碼

  • JVM在解釋執行位元組碼的時候,不會立即對它進行編譯。主要原因有兩個:
    • 如果程式碼只執行一次,對程式碼進行編譯得不償失(編譯之後還要執行程式碼)。
    • 程式碼執行的次數越多,JVM可以獲取到的資訊就越多。JVM就可以在編譯程式碼的時候採用更多的優化手段。比如:JVM經常執行equals()方法,b = obj1.equals(obj2)JVM需要根據obj1找到它的型別,然後才知道應該執行那個equals
      函式。這個過程是比較費時的,為了加快執行速度,編譯程式碼的時候可以將型別查詢的過程優化掉,直接執行String.equals(obj2)。不過,在實際程式碼中,可能不會這麼簡單,obj1的型別會發生變化。但是隻要程式碼執行次數夠多,優化後,效能就會有比較大的提升。

基本調優:ClientServer

  • 即時編譯器有兩種型別,clientserver一般情況下,對編譯器進行優化,唯一要做的就是選擇那一類編譯器。
  • 可以通過在啟動java的命令中,傳入引數(-client-server)來選擇編譯器(C1C2)。這兩種編譯器的最大區別就是,編譯程式碼的時間點不一樣。client編譯器(C1)會更早地對程式碼進行編譯,因此,在程式剛啟動的時候,
    client編譯器比server編譯器執行得更快。而server編譯器會收集更多的資訊,然後才對程式碼進行編譯優化,因此,server編譯器最終可以產生比client編譯器更優秀的程式碼。
  • 可能大家都有一個困擾,JVM為什麼要將編譯器分為clientserver,為什麼不在程式啟動時,使用client編譯器,在程式執行一段時間後,自動切換為server編譯器?其實,這種技術是存在的,一般稱之為: tiered compilationJava7 Java 8可以使用選項-XX:+TieredCompilation來開啟(-server選項也要開啟)。在Java 8中,-XX:+TieredCompilation預設是開啟的。

啟動優化

  • 對於不同型別的應用,使用不同編譯器的啟動時間:
                  

可以看到,對於中等大小的GUI應用,使用client編譯器,啟動時間最短,有將近38.5%的提升。對於大型應用,使用各種編譯器的啟動時間差別不大。

批處理應用優化

  • 下圖是對第2章中股票應用的效能測試結果,第1列是股票的個數:
                    

觀察上圖,可以得出以下結論:

1)當股票數比較少的時候(1~100),client編譯器完成的最快。

2)當股票數很多的時候,使用server編譯器就變得更快了。

3tiered compilation總是比server編譯器要快(和client比,即使在股票數很少的情況下,效能相差也不大),這是因為,tieredcompilation會對一些執行次數較少的程式碼也進行編譯(編譯後比解釋執行要快)。

長時間執行的應用優化

  • 還是利用第2章的例子,這次使用第2章提供的servlet,下圖是在不同“warm-up period下(也就是重要的程式碼段都被編譯了)應用的吞吐量(每秒處理請求的數目)。
             

觀察上圖,可以得出以下結論:

        1)由於測試周期是60秒,所以,即使warm-upperiod0sserver編譯器也有足夠的時間來發現熱點程式碼並進行編譯,所以,server編譯器總是比client編譯器的吞吐量要高。

        2)同樣的tiered compilation總是比server編譯器的吞吐量要高(原因見上文,批處理應用優化)。

Java和JIT編譯器版本

  • JIT編譯器主要有以下3大類:
    • 32-bitclient版本(-client)
    • 32-bit的server版本(-server)
    • 64-bit的server版本(-d64)
  • 那到底是選擇32bit版本還是64bit版本呢?是不是作業系統是64bit的就選擇64位版本呢?

答案並不是這樣的,而是需要根據情況確定。

使用32bit版本的優點主要有兩個:1)佔用記憶體少,因為對應引用都是32位的 2)效能高,因為CPU操作32位的記憶體引用要比操作64位的記憶體引用要快。缺點也有兩個:1)使用堆記憶體的大小不能超過4GB(windows為3GB,Linux為3.5GB) 2)程式中,如果使用了大量的longdouble變數,不能充分使用64位暫存器,不過這種情況比較少見。

一般來說,32bitJVM上的32bit編譯器要比同樣配置的64bit編譯器要快5%~20%

  • 64bit版本的JVM無法使用32bit的編譯器
  • 如果在64bitJVM啟動時,使用-client引數,JVM還是會使用server編譯器;如果在32-bitJVM啟動時,使用-server引數,則會報錯,提醒使用者,JVM不支援對應的編譯器。

上圖是對不同平臺下,使用不同引數對應的編譯器。

  • 對於JAVA8來說,server編譯器是預設編譯器,tiered compilation也是預設開啟的。
  • 對於不同平臺,如果沒有傳入-server-client引數,則會使用預設的編譯器版本,具體如下圖所示:

編譯器的中間段優化

  • 一般情況下,編譯器的優化就是選擇JVM啟動的引數;但是在以下情況還是要進行額外優化的。

CodeCache優化

  • JVM在編譯程式碼的時候,會在CodeCache中儲存一些彙編指令。由於CodeCache的大小是固定,一旦它被填充滿了,JVM就無法編譯其它程式碼了。如果CodeCache很小,就會導致部分熱點程式碼沒有被編譯,應用的效能將會急劇下降(執行解釋性程式碼)。
  • 如果JVM使用了clienttiered compilation編譯器,更可能會出現問題;因為它們會對很多類都進行編譯。當CodeCache滿了的時候,JVM會列印類似於下面的告警資訊:

Java HotSpot(TM) 64-Bit Server VM warning: CodeCacheis full.

Compiler has been disabled.

Java HotSpot(TM) 64-Bit Server VM warning: Tryincreasing the

code cache size using -XX:ReservedCodeCacheSize=

  • 各個版本的JVMCodeCache的預設大小,如下圖所示:

從上圖可以看到,Java7CodeCache通常是不夠的,一般都需要進行加大。到底增大到多少,這個比較難給出精確值,一般是預設值的2倍或4倍。

  • CodeCache的最大大小,可以通過 -XX:ReservedCodeCacheSize=N來指定,初始大小使用-XX:InitialCodeCacheSize=N來指定。有初始大小是會自動增加的,所以一般不需要設定-XX:InitialCodeCacheSize引數。
  • 既然CodeCache大小會有最大值,我們是否可以設定一個很大的值呢?這個主要看系統資源是否足夠。對於32bit的JVM,由於虛擬記憶體總大小為4GB,所以這個值不能設定的太大;對於64bit的JVM,這個值的大小設定一般不受限制的。

編譯閾值

  • 是否對程式碼進行編譯受兩個計數器的影響:1)方法的呼叫次數 2)方法內迴圈的次數JVM執行一個JAVA方法的時候,它都會檢查這兩個計數器,以便確定是否需要對方法進行編譯。
  • JVM執行一個JAVA方法的時候,它會檢查這兩個計數器值的總和,然後確定這個方法是否需要進行編譯。如果需要進行編譯,這個方法會放入編譯佇列。這種編譯沒有官方的名字,一般稱之為標準編譯
  • 如果方法裡面是一個迴圈,雖然方法只調用一次,但是迴圈在不停執行。此時,JVM會對這個迴圈的程式碼進行編譯。這種編譯稱之為棧上替換OSRon-stack replacement)。因為即使迴圈被編譯了,這還不夠,JVM還需要能夠在迴圈正在執行的時候,轉為執行編譯後的程式碼。JVM採用的方式就是將編譯後的程式碼替換當前正在執行的方法位元組碼。由於方法在棧上執行,所以這個操作又稱之為:棧上替換
  • 可以通過配置 -XX:CompileThreshold=N來確定計數器的閾值,從而控制編譯的條件。但是如果降低了這個值,會導致JVM沒有收集到足夠的資訊就進行了編譯,導致編譯的程式碼優化不夠(不過影響會比較小)。
  • 如果-XX:CompileThreshold=N配置的兩個值(一個大,一個小)在效能測試方面表現差不多,那麼會推薦使用小一點的配置,主要有兩個原因:
    • 可以減小應用的warm-upperiod
    • 可以防止某個方法永遠得不到編譯。這是因為JVM會週期(每到安全點的時候)的對計數進行遞減。如果閾值比較大,並且方法週期性呼叫的時間較長,導致計數永遠達不到這個閾值,從而不會進行編譯。

探尋編譯過程

  • 選項-XX:+PrintCompilation可以開啟編譯日誌,當JVM對方法進行編譯的時候,都會列印一行資訊,什麼方法被編譯了。具體格式如下:

timestamp compilation_idattributes (tiered_level) method_name size deopt

  • timestamp是編譯時相對於JVM啟動的時間戳
  • compliation_id是內部編譯任務的ID,一般都是遞增的
  • attributes由5個字母組成,用來顯示程式碼被編譯的狀態:
    • %:編譯是OSR
    • s:方法是synchronized
    • !:方法有異常處理
    • b:編譯執行緒不是後臺執行的,而是同步的(當前版本的JVM應該不會列印這個狀態了)
    • n:表示JVM產生了一些輔助程式碼,以便呼叫native方法
  • tiered_level:採用tiered compilation編譯器時才會列印。
  • method_name:被編譯的方法名稱,格式是:classname::method
  • size: 被編譯的程式碼大小(單位:位元組),這裡的程式碼是Java位元組碼。
  • deopt:如果發生了去優化,這裡說明去優化的型別。(見下文的說明)

小技巧:使用jstat來檢視編譯器的行為

% jstat -compiler 5003

Compiled Failed Invalid Time FailedTypeFailedMethod

206 0 01.97 0

5003JVM的程序

相關推薦

Java效能優化指南系列(理解JIT編譯器

即時編譯器概述 編譯器在編譯過程中通常會考慮很多因素。比如:彙編指令的順序。假設我們要將兩個暫存器的值進行相加,執行這個操作一般只需要一個CPU週期;但是在相加之前需要將資料從記憶體讀到暫存器中,這個操作是需要多個CPU週期的。編譯器一般可以做到,先啟動資料載入操作,然後執

Java多線程編程模式實戰指南Two-phase Termination模式

增加 row throws mgr 額外 finally join table 還需 停止線程是一個目標簡單而實現卻不那麽簡單的任務。首先,Java沒有提供直接的API用於停止線程。此外,停止線程時還有一些額外的細節需要考慮,如待停止的線程處於阻塞(等待鎖)或者等待狀態(等

Java多執行緒程式設計模式實戰指南Two-phase Termination模式

 本文由本人首次釋出在infoq中文站上:http://www.infoq.com/cn/articles/java-multithreaded-programming-mode-two-phase-termination 停止執行緒是一個目標簡單而實現卻不那麼簡單的

Java效能優化指南(四)GC收集器導論

本章主要介紹垃圾收集器的基礎知識。為了提升效能,如果需要重寫程式碼,那肯定需要花費很大的精力,所以一般都是在不得已的情況下才會這麼做。實踐證明,對垃圾收集器進行調優可以對應用帶來比較大的效能提升,它也是效能工程師對應用進行調優的重要手段。當前Java虛擬機器主要有4類垃圾收

Java設計模式簡介(行為型模式(上

本章講到第三種設計模式——行為型模式,共11種:策略模式、模板方法模式、觀察者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、直譯器模式。 先來張圖,看看這11中模式的關係: 第一類:通過父類與子類的關係進行實現。第二類:兩個類之間。第三類:類的狀態。第

java調Python指令碼(傳引數問題

  java調Python的指令碼,我們需要將java這邊的引數傳到Python那邊使用,所以就需要傳參。 Python需要匯入系統包 import sys,然後使用 sys.argv[i] 接收java傳過來的引數就可以。 1、Python程式碼:

Java類集框架(Set子介面

Set子介面只是簡單地繼承了Collection介面,並沒有擴充其他的方法。Set集合中不允許儲存重複的資料。在Set介面下有兩個常用的子類:HashSet、TreeSet。HashSet是雜湊存放資料,而TreeSet是有序存放的子類,預設按照字母的升序排列。在實際開發中如果沒有排序要求,

Java開發學習心得(專案結構

3 專案結構 經過前面一系列學習,差不多對Java的開發過程有了一定的瞭解,為了能保持一個良好的專案結構,考慮到接下來要進行開發,還需要學習一下Java的專案結構 下面以兩個專案結構為參照 圖1 圖2 第一個是我自己學習時的Demo,一邊學一邊建檔案,應該有些錯誤的地方,第二張是從網上看到的

Java開發學習心得(項目結構

dao 有一個 界面 sin 後綴 說過 所有 保持 工程 3 項目結構 經過前面一系列學習,差不多對Java的開發過程有了一定的了解,為了能保持一個良好的項目結構,考慮到接下來要進行開發,還需要學習一下Java的項目結構 下面以兩個項目結構為參照 圖1 圖2 第一個是

Java 泛型總結(萬用字元的使用

簡介 前兩篇文章介紹了泛型的基本用法、型別擦除以及泛型陣列。在泛型的使用中,還有個重要的東西叫萬用字元,本文介紹萬用字元的使用。 這個系列的另外兩篇文章: Java 泛型總結(一):基本用法與型別擦除 Java 泛型總結(二):泛型與陣列 陣列的協變 在瞭解萬用字

java後端開發(開發框架解讀

前言 本篇講述後端開發中用到的主要框架,旨在瞭解框架的意義和種類 什麼是框架 我相信對於大部分開發人員來說,框架再熟悉不過了,但是要給未接觸過的人講明白恐怕是非常難的,本節的目的就是讓讀者對於框架有50%的認知,剩下的需要在實踐中體會。 框架與P

前端效能優化總結(

常用的優化有兩部分 第一:面向內容的優化 1. 減少 HTTP 請求 2. 減少 DNS 查詢 3. 避免重定向 4. 使用 Ajax 快取 5. 延遲載入元件 6. 預先載入元件 7. 減少 DOM 元素數量 8. 切分元件到多個域 9. 最小化 iframe

Java設計模式詳談(觀察者

        觀察者模式又叫作(釋出-訂閱)模式,屬於行為型模式的一種,它定義了一種一對多的關係,當多個觀察者同時監聽到被觀察者出現的變化,就會作出對應的處理。      

Android效能優化典範(

Android效能優化典範的課程最近更新到第三季了,這次一共12個短視訊課程,包括的內容大致有:更高效的ArrayMap容器,使用Android系統提供的特殊容器來避免自動裝箱,避免使用列舉型別,注意onLowMemory與onTrimMemory的回撥,避免記憶體洩漏,

JAVA練手專案(坦克大戰遊戲原始碼

     經過幾天的練習和研究終於自己能寫出坦克大戰遊戲了,寫完這個程式後感覺收穫了很多東西,對JAVA的知識又有了一定的增長,接下來還準備繼續寫幾個小專案來練習J2SE     由於程式碼太長就不發在部落格裡了,我上傳到了資源下載裡,有需要的朋友大家可以去下載  

MySQL 資料庫效能優化之(索引優化

大家都知道索引對於資料訪問的效能有非常關鍵的作用,都知道索引可以提高資料訪問效率。 為什麼索引能提高資料訪問效能?他會不會有“副作用”?是不是索引建立越多,效能就越好?到底該如何設計索引,才能最大限度的發揮其效能? 這篇文章主要是帶著上面這幾個問題來做一個簡要的分析,

機器學習入坑指南簡單線性迴歸

學習了「資料預處理」之後,讓我們一起來實現第一個預測模型——簡單線性迴歸模型。 一、理解原理 簡單線性迴歸是我們接觸最早,最常見的統計學分析模型之一。 假定自變數 xxx與因變數 yyy 線性相關,我們可以根據一系列已知的 (x,y)(x,y)(x,y) 資料

Backbone入門指南Events(事件管理

6. Events (事件管理) 從這一章開始,我將正式介紹Backbone的內容,過程中會有許多例子和程式碼,你應該將這些程式碼複製到你的頁面,並檢視它們的執行效果。 我介紹的第一個模組是Ba

JAVA效能優化筆記(1

**JAVA效能優化筆記(1)** 1.減少GC的壓力,GC是一個優先順序比較低的守護執行緒。主要是回收我們的堆記憶體 2.儘量的避免我們的new操作。 一旦我們的new操作過多,會導致GC壓力過大。因為用new操作會

效能優化總結(一對多join的級聯查詢

最近在對一個已經運行了4年的老專案進行效能優化,這是一個app,我主要還是從後臺優化。這個專案後臺提供的API並不多,只有十幾個,但是效能非常差。有些API連每分鐘2000的request都扛不住,雖然後臺的併發node已經增加到8個。一看程式碼就知道為什麼了,原來這些AP