一 基本知識
在性能調優之前,我們首先來了解一下性能是什么?關于性能,我想每個學習過 Java 的人都能列出幾點,甚至可以夸夸其談。在《 Java TM Platform Performance 》一書中,定義了如下五個方面來作為評判性能的標準:
1) 運算的性能——哪一個算法的執行性能最好?
2) 內存的分配——程序運行時需要耗費多少內存?
3) 啟動的時間——程序啟動需要多長時間?這在 Web 項目中的影響不大,但要注意部分程序需要部署或運行在客戶端時的情形(比如 applet 程序)。
4) 程序的可伸縮性——在壓力負載的情況下,程序的性能如何?
5) 性能的感知——用戶在什么情況下會覺得程序的性能不好?
以上五個方面,在具體的使用場景可以有選擇的去評判。至于這五方面的性能調優,在后續的章節中將會陸續的給以相應的性能調優策略。
1.2 調優的規則
我們只需要關心對我們程序有影響,可以察覺到的性能問題,而不是每一個類中的每一個方法我們都需要想方設法的提高性能。如果程序的性能沒有達到我們所期望的要求,我們才需要考慮如何優化性能。同樣的,晦澀的代碼雖然提高了程序的性能,但同時可能帶給我們的是維護的噩夢。我們需要折中的考慮以上兩種情況,使得程序的代碼是優美的,并且運行的足夠快,達到客戶所期望的性能要求。
優化代碼甚至會導致不良的結果, Donald Knuth (一位比較牛比較有影響的人物,具體是誰,我也忘了,誰知道,可以告訴我一下,謝謝!)曾說過,“ Premature optimization is the root of all evil” 。在開始性能調優前,需要先指出不優化代碼的一些理由。
1) 如果優化的代碼已經正常工作,優化后可能會引入新的 bug ;
2) 優化代碼趨向于使代碼更難理解和維護;
3) 在一個平臺上優化的代碼,在另一個平臺上可能更糟;
4) 花費很多時間在代碼的優化上,提高了很少的性能,卻導致了晦澀的代碼。
確實,在優化前,我們必須認真的考慮是否值得去優化。
1.3 調優的步驟
一般我們提高應用程序的性能劃分為以下幾個步驟:
1) 明確應用程序的性能指標,怎樣才符合期望的性能需求;
2) 在目標平臺進行測試;
3) 如果性能已經達到性能指標, Stop ;
4) 查找性能瓶頸;
5) 修改性能瓶頸;
6) 返回到第 2 步。
二 JDK 調優
2.1 選擇合適的 JDK 版本
不同版本的 JDK ,甚至不同廠家的 JDK 可能都存在著很大的差異,對于性能優化的程度不同。一般來說,盡可能選擇最新發布的穩定的 JDK 版本。最新的穩定的 JDK 版本相對以前的 JDK 版本都會做一些 bug 的修改和性能的優化工作。
2.2 垃圾收集 Java 堆的優化
垃圾收集就是自動釋放不再被程序所使用的對象的過程。當一個對象不再被程序所引用時,它所引用的堆空間可以被回收,以便被后續的新對象所使用。垃圾收集器必須能夠斷定哪些對象是不再被引用的,并且能夠把它們所占據的堆空間釋放出來。如果對象不再被使用,但還有被程序所引用,這時是不能被垃圾收集器所回收的,此時就是所謂的“內存泄漏”。監控應用程序是否發生了內存泄漏,有一個非常優秀的監控工具推薦給大家——Quest 公司的 JProbe 工具,使用它來觀察程序運行期的內存變化,并可產生內存快照,從而分析并定位內存泄漏的確切位置,可以精確定位到源碼內。這個工具的使用我在后續的章節中還會做具體介紹。
Java 堆是指在程序運行時分配給對象生存的空間。通過 -mx/-Xmx 和 -ms/-Xms 來設置起始堆的大小和最大堆的大小。根據自己 JDK 的版本和廠家決定使用 -mx 和 -ms 或 -Xmx 和 -Xms 。 Java 堆大小決定了垃圾回收的頻度和速度, Java 堆越大,垃圾回收的頻度越低,速度越慢。同理, Java 堆越小,垃圾回收的頻度越高,速度越快。要想設置比較理想的參數,還是需要了解一些基礎知識的。
Java 堆的最大值不能太大,這樣會造成系統內存被頻繁的交換和分頁。所以最大內存必須低于物理內存減去其他應用程序和進程需要的內存。而且堆設置的太大,造成垃圾回收的時間過長,這樣將得不償失,極大的影響程序的性能。以下是一些經常使用的參數設置:
1) 設置 -Xms 等于 -XmX 的值;
2) 估計內存中存活對象所占的空間的大小,設置 -Xms 等于此值, -Xmx 四倍于此值;
3) 設置 -Xms 等于 -Xmx 的 1/2 大小;
4) 設置 -Xms 介于 -Xmx 的 1/10 到 1/4 之間;
5) 使用默認的設置。
大家需要根據自己的運行程序的具體使用場景,來確定最適合自己的參數設置。
除了 -Xms 和 -Xmx 兩個最重要的參數外,還有很多可能會用到的參數,這些參數通常強烈的依賴于垃圾收集的算法,所以可能因為 JDK 的版本和廠家而有所不同。但這些參數一般在 Web 開發中用的比較少,我就不做詳細介紹了。在實際的應用中注意設置 -Xms 和 -Xmx 使其盡可能的優化應用程序就行了。對于性能要求很高的程序,就需要自己再多研究研究 Java 虛擬機和垃圾收集算法的機制了。可以看看曹曉鋼翻譯的《深入 Java 虛擬機》一書。
WEB 服務總是莫名其妙的運行一段時間后 JVM 直接 OutOfMemory 錯誤,內存泄漏的問題不容易查找,本文就一些查找內存泄露基本知識做個總結,未涉及到具體案例的分析。
Heap
PSYoungGen total 44928K, used 916K [0x4e3c0000, 0x50fe0000, 0x51b10000)
eden space 44736K, 2% used [0x4e3c0000,0x4e4a5318,0x50f70000)
from space 192K, 0% used [0x50f70000,0x50f70000,0x50fa0000)
to space 192K, 0% used [0x50fb0000,0x50fb0000,0x50fe0000)
PSOldGen total 453312K, used 125529K [0x32910000, 0x4e3c0000, 0x4e3c0000)
object space 453312K, 27% used [0x32910000,0x3a3a6498,0x4e3c0000)
PSPermGen total 65536K, used 65535K [0x2e910000, 0x32910000, 0x32910000)
object space 65536K, 99% used [0x2e910000,0x3290fff8,0x32910000)
|
permanent space 持久空間 : 用于類和方法對象的存儲。 spring 在 AOP 時使用 CBLIB 會動態產生很多類,當類太多,超過 MaxPermSize 的時候,就會拋出此異常。 參數問題 可以設置 jvm 啟動參數 : PermSize, MaxPermSize 。程序問題就要進行內存分析了,詳見下文。
Heap
PSYoungGen total 88320K, used 67673K [0x44880000, 0x4ba40000, 0x4ba40000)
eden space 61952K, 100% used [0x44880000,0x48500000,0x48500000)
from space 26368K, 21% used [0x48500000,0x48a96490,0x49ec0000)
to space 24512K, 16% used [0x4a250000,0x4a6283e0,0x4ba40000)
PSOldGen total 932096K, used 582090K [0x0ba40000, 0x44880000, 0x44880000)
object space 932096K, 62% used [0x0ba40000,0x2f2b2a78,0x44880000)
PSPermGen total 131072K, used 35124K [0x03a40000, 0x0ba40000, 0x0ba40000)
object space 131072K, 26% used [0x03a40000,0x05c8d330,0x0ba40000)
|
eden space 使用率 100% ,總是被占滿,參數問題 可以設置 jvm 啟動參數 : Xms, Xmx 。程序問題就要進行內存分析了,詳見下文。
jstat -gcutil pid 1000 20
異常情況的例子
jstat -gcutil pid 1000 20
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 99.99 82.51 53.11 2409 1.205 10117 7250.393 7251.598
0.00 0.00 83.42 82.55 53.10 2409 1.205 10118 7252.650 7253.855
0.00 0.00 56.06 82.46 53.10 2410 1.205 10120 7254.467 7255.672
0.00 0.00 32.11 82.55 53.10 2411 1.205 10121 7256.673 7257.877
0.00 0.00 99.99 82.55 53.10 2412 1.205 10123 7257.026 7258.231
0.00 0.00 76.00 82.50 53.10 2412 1.205 10124 7259.241 7260.446
|
這個數據顯示 Full GC 頻繁發生。
|
正常情況的例子
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
|
參數含義:
S0 : Heap 上的 Survivor space 0 段已使用空間的百分比
S1 : Heap 上的 Survivor space 1 段已使用空間的百分比
E : Heap 上的 Eden space 段已使用空間的百分比
O : Heap 上的 Old space 段已使用空間的百分比
P : Perm space 已使用空間的百分比
YGC :從程序啟動到采樣時發生 Young GC 的次數
YGCT : Young GC 所用的時間 ( 單位秒 )
FGC :從程序啟動到采樣時發生 Full GC 的次數
FGCT : Full GC 所用的時間 ( 單位秒 )
GCT :用于垃圾回收的總時間 ( 單位秒 )
|
在 windows 下,使用 tasklist
|
在 Linux 下,使用 ps –aux
|
可以通過命令:
jmap -dump:file=a.hprof pid
|
也可以通過 jconsole 的圖形界面操作。
Jconsole 打開后在造作下選擇 dumpHeap, 輸入參數 p0,p1;p0 表示 dump 出來的文件路徑,后綴為 .hprof ;p1 設為 true ,表示只分析活著的對象。

目前有很多用來分析 Java 內存對象的工具,如收費的工具有 jprofiler, 而像 Eclipse MAT 則是優秀的內存對象分析開源工具 . 它們對于分析內存溢出問題非常有用。以下是一個安裝使用 Eclipse MAT 的簡單例子。
http://download.eclipse.org/technology/mat/latest/update-site/


4 JDK 自帶的 JVM 查看分析工具 jps 、 jmap 、 jstat 、 jconsole
Java 進程查看工具,實際上它和 Unix/Linux 上面的 ps 命令的功能差不多
jmap 是一個可以輸出所有內存中對象的工具 .
* -dump:format=b,file=<filename> 轉存堆內存到本地文件。
* -histo 打印堆里每個類的情況,包含內存占用大小、對象數量及完整類名。 VM 的內部類以 "*" 開頭。
例子:
jmap -histo pid>a.log
|
jmap -dump: file=a.hprof pid
|
查看 a.log
num #instances #bytes class name
--------------------------------------
1: 427398 14458448 [I
2: 178798 6830216 [C
3: 50278 6668512 <constMethodKlass>
4: 179924 4318176 java.lang.String
5: 50278 4026648 <methodKlass>
6: 15244 3894200 [B
7: 47809 1773776 [Ljava.lang.Object;
...
Total 1645187 81806088
|
說明:
#instance 是對象的實例個數
#bytes 是總占用的字節數
class name 對應的就是 Class 文件里的 class 的標識
B 代表 byte
C 代表 char
D 代表 double
F 代表 float
I 代表 int
J 代表 long
Z 代表 boolean
前邊有 [ 代表數組, [I 就相當于 int[]
對象用 [L+ 類名表示
|
jstat 是 vm 的狀態監控工具,監控的內容有類加載、運行時編譯及 GC 。
使用時,需加上查看進程的進程 id ,和所選參數。以下詳細介紹各個參數的意義。
jstat -class pid: 顯示加載 class 的數量,及所占空間等信息。
jstat -compiler pid: 顯示 VM 實時編譯的數量等信息。
jstat -gc pid: 可以顯示 gc 的信息,查看 gc 的次數,及時間。其中最后五項,分別是 young gc 的次數,young gc 的時間, full gc 的次數, full gc 的時間, gc 的總時間。
jstat -gccapacity: 可以顯示, VM 內存中三代( young,old,perm )對象的使用和占用大小,如: PGCMN顯示的是最小 perm 的內存使用量, PGCMX 顯示的是 perm 的內存最大使用量, PGC 是當前新生成的 perm 內存占用量, PC 是但前 perm 內存占用量。其他的可以根據這個類推, OC 是 old 內純的占用量。
jstat -gcnew pid:new 對象的信息。
jstat -gcnewcapacity pid:new 對象的信息及其占用量。
jstat -gcold pid:old 對象的信息。
jstat -gcoldcapacity pid:old 對象的信息及其占用量。
jstat -gcpermcapacity pid: perm 對象的信息及其占用量。
jstat -util pid: 統計 gc 信息統計。
jstat -printcompilation pid: 當前 VM 執行的信息。
除了以上一個參數外,還可以同時加上 兩個數字,如: jstat -printcompilation 3024 250 6 是每 250毫秒打印一次,一共打印 6 次,還可以加上 -h3 每三行顯示一下標題。
例子:
jstat -gcutil pid 1000 20
|
一個 java GUI 監視工具,可以以圖表化的形式顯示各種數據。并可通過遠程連接監視遠程的服務器 VM 。