Java性能權威指南讀書筆記--之一

JIT(即時編譯)

解釋型代碼:程序可移植,相同的代碼在任何有適當解釋器的機器上,都能運行,可是速度慢。
編譯型代碼:速度快,電視不一樣CPU平臺的代碼沒法兼容。
java則是使用java的編譯器先將其編譯爲class文件,也就是字節碼;而後將字節碼交由jvm(java虛擬機)解釋執行。因爲這個編譯是在程序執行時進行的,所以被稱爲「即便編譯」。java

熱點編譯

對於程序來講,一般只有一部分代碼被常常執行,而應用的性能就取決於這些代碼執行得有多快。這些關鍵代碼段被稱爲應用的熱點,代碼執行得越多就被認爲是越熱。
所以JVM執行代碼時,並不會當即編譯代碼:緩存

  1. 若是代碼只執行一次,那編譯徹底就是浪費精力。對於只執行一次的代碼,解釋執行Java字節碼比先編譯而後執行的速度快。
  2. JVM執行特定方法或者循環的次數越多,它就會越瞭解這段代碼。這使得JVM能夠在編譯代碼時進行大量優化。

分層編譯

Client編譯器和server編譯器主要的區別在於編譯代碼的時機不一樣。client編譯器開啓編譯比server編譯器要早。這意味着在代碼執行的開始階段,client編譯器比server編譯器要快,由於它的編譯代碼相比server編譯器而言要多。
分層編譯是綜合了client和server的優勢。在開啓分層編譯(-XX:+TieredCompilation)後代碼先由client編譯器編譯,隨着代碼變熱,由server編譯器從新編譯。jvm

調優代碼緩存

JVM編譯代碼時,會在代碼緩存中保留編譯以後的彙編語言指令集。代碼緩存的大小固定,因此一旦填滿,JVM就不能編譯更多代碼了。
也就是說,若是代碼緩存太小,那麼就會有一些熱點代碼被編譯了,而其餘沒有,最終致使應用的大部分代碼都是解釋運行(很是慢)。這個問題在使用client編譯器或進行分層編譯時很常見。
當代碼緩存填滿時,JVM一般會發出如下警告:性能

Java HotSopt(TM) 64-Bit Server VM warning:CodeCache is full.Compiler has bean disabled.
Java HotSopt(TM) 64-Bit Server VM warning:Try increasing the code cache size using -XX:ReservedCodeCacheSize=

各平臺代碼緩存的默認大小:優化

jvm jdk版本 大小
32位client Java8 32MB
32位client 分層編譯,Java8 240MB
64位client 分層編譯,Java8 240MB
32位client Java7 32MB
32位server Java7 32MB
64位server Java7 48MB
64位server 分層編譯,Java7 48MB

若是代碼緩存設爲1GB,JVM就會保留1GB的本地內存空間。若是是32位JVM,那麼進程佔用的總內存不能超過4GB(包括Java堆、JVM自身全部代碼佔用空間、分配給應用的本地內存、代碼緩存)。
經過jconsole Memory(內存)面板的Memory Pool Code Cache圖表,能夠監控代碼緩存。線程

編譯閾值

一旦代碼執行到必定次數,且達到了編譯閾值,編譯器就能夠得到足夠的信息編譯代碼了。
編譯是基於兩種JVM計數器的:方法調用計數器和方法中的循環回邊計數器。回邊實際上能夠看做是循環完成執行的次數。
棧上替換:JVM能夠在方法循環運行時進行編譯,並在循環代碼編譯結束以後,JVM替換還在棧上的代碼,循環的下一次迭代就會執行快的多的代碼。
標準編譯由-XX:CompileThreshold=N標誌觸發。使用client編譯器時,N的默認值是1500,使用server編譯器時爲10000。
計數器會隨着時間而減小,因此計數器只是方法或循環最新熱度的度量。由此帶來一個反作用是,執行不太頻繁的代碼可能永遠不會編譯。日誌

檢測編譯過程

-XX:+PrintCompilation
若是開啓PrintCompilation,每次編譯一個方法(或循環)時,JVM就會打印一行被編譯的內容信息。
絕大多數編譯日誌的行具備如下格式:
timestamp compilation_id attributes (tiered_level) method_name size deopt
timestamp表示編譯完成的時間
compilation_id內部的任務ID
attributes是一組5個字符長的串,表示代碼編譯的狀態。若是給定的編譯被賦予了特定屬性,就會打印下面列表中所顯示的字符,不然該屬性就打印一個空格。
* % :編譯爲OSR
* s :方法是同步的
* !:方法有異常處理器
* b :阻塞模式時發生的編譯
* n:爲封裝本地方法所發生的編譯
tiered_level 若是程序沒有使用分紅編譯的方式運行則爲空,不然爲數字,代表所完成編譯的級別
method_name格式爲:ClassName::method
而後是編譯後代碼大小(單位是字節)
最後,在某些狀況下,編譯日誌的結尾會有一條信息,代表發生了某種逆優化,一般是「made not entrant」或」made zombie」code

135    1     n 0       java.lang.Thread::currentThread (native)   (static)
    136    2       3       java.util.Arrays::copyOf (19 bytes)
    136    7       3       sun.nio.cs.UTF_8$Encoder::encode (359 bytes)
    137    8       2       java.lang.String::hashCode (55 bytes)

使用jstat -compiler 進程ID 也能夠看有多少方法被編譯
使用jstat -printcompilation 5003 1000 表示進程ID爲5003的程序每1秒輸出一次最近被編譯的方法server

編譯器線程

當方法(或循環)適合編譯時,就會進入到編譯隊列。隊列則由一個或多個後臺線程處理。編譯隊列是一種優先隊列,即調用計數次數多的方法有更高的優先級。
當開啓分層編譯時,JVM默認開啓多個client和server線程。對象

cpu數量 C1的線程數(client) C2的線程數(server)
1 1 1
2 1 1
4 1 2
8 1 2
16 2 6
32 3 7
64 4 8
128 4 10

編譯器的線程數可經過-XX:CICompilerCount=N標誌來設置。對於分層編譯來講,設置的值中三分之一將用來處理client編譯器隊列,其他的線程(至少一個)用來處理server編譯器隊列。
使用分層編譯時,線程數很容易超過系統限制,特別是有多個JVM同時運行的時候。在這種狀況下,減小線程數有助於提升總體的吞吐量(儘管代價多是熱身期會持續得更長)。

方法內聯

public class Point{
    private int x,y;
    public int getX(){ return x; }
    public void setX(int i){ x = i;}
}

若是你寫下面的代碼

Point p = getPoint();
p.setX(p.getX()*2);

編譯後的代碼本質上執行的是:

Point p = getPoint();
p.x = p.x *2;

方法是否內聯取決於它有多熱以及它的大小。
-XX:MaxInlineSize=N默認是35字節,即只有方法小於35字節時第一次調用方法時就會被內聯。
-XX:MaxFreqInlineSize=N默認是325字節,即只有當一個方法頻繁被調用而且小於325字節時會被內聯。

逃逸分析

-XX:+DoEscapeAnalysis默認爲true。逃逸分析可讓JVM對一個對象根據代碼來進行優化。

  1. 棧上分配
    咱們都知道Java中的對象都是在堆上分配的,而垃圾回收機制會回收堆中再也不使用的對象,可是篩選可回收對象,回收對象還有整理內存都須要消耗時間。若是可以經過逃逸分析肯定某些對象不會逃出方法以外,那就可讓這個對象在棧上分配內存,這樣該對象所佔用的內存空間就能夠隨棧幀出棧而銷燬,就減輕了垃圾回收的壓力。
  2. 同步消除
    若是發現某個對象只能從一個線程可訪問,那麼在這個對象上的操做能夠不須要同步。
  3. 標量替換
    Java虛擬機中的原始數據類型(int,long等數值類型以及reference類型等)都不能再進一步分解,它們就能夠稱爲標量。相對的,若是一個數據能夠繼續分解,那它稱爲聚合量,Java中最典型的聚合量是對象。若是逃逸分析證實一個對象不會被外部訪問,而且這個對象是可分解的,那程序真正執行的時候將可能不建立這個對象,而改成直接建立它的若干個被這個方法使用到的成員變量來代替。拆散後的變量即可以被單獨分析與優化,能夠各自分別在棧幀或寄存器上分配空間,本來的對象就無需總體分配空間了。

小結

  1. 不用擔憂小方法,特別是getter和setter,由於它們容易內聯。
  2. 須要編譯的代碼在編譯隊列中,隊列中代碼越多,程序打到最佳性能的時間越久。
  3. 雖然代碼緩存的大小能夠調整,但它仍然是有限的資源
  4. 代碼越簡單,優化越多。
相關文章
相關標籤/搜索