1. 程式人生 > >認真看完這篇文章,JVM將不再是你的短板

認真看完這篇文章,JVM將不再是你的短板

前言

 

想起當年還是個菜鳥的時候,看了許多帖子,裡面的Java大神們都在說:JVM調優是JavaIT人進階所必備的知識。

於是就買了書和教程去看,但是發現對於調優這個話題看書效率奇低,有些內容反覆看了又看,回想起來腦子裡卻不剩什麼。

為了避免大家出現類似的情況,特地編輯了一篇JVM相關文章供大家參考學習,希望能幫助到想要進步的你。

 

一:虛擬機器記憶體圖解

 

JAVA程式執行與虛擬機器之上,執行時需要記憶體空間。虛擬機器執行JAVA程式的過程中會把它管理的記憶體劃分為不同的資料區域方便管理。

 

虛擬機器管理記憶體資料區域劃分如下圖:

 

 

資料區域分類:

 

方法區:(Method Area)

虛擬機器棧:(VM Stack)

本地方法棧 :(Native Method Stack)

堆:(Heap)

程式計數器 :(Program Counter Register)

直接記憶體:(Direct Memory)

 

說明:

 

1. 程式計數器

 

行號指示器,位元組碼指令的分支、迴圈、跳轉、異常處理、執行緒恢復(CPU切換),每條執行緒都需要一個獨立的計數器,執行緒私有記憶體互不影響,該區域不會發生記憶體溢位異常。

 

2. 虛擬機器棧

 

是執行緒私有的,宣告週期與執行緒相同,虛擬機器棧是Java方法執行的記憶體模型,每個方法被執行時都會建立一個棧幀,即方法執行期間的基礎資料結構,棧幀用於儲存:區域性變量表、運算元棧、動態連結、方法出口等,每個方法執行中都對應虛擬機器棧幀從入棧到處棧的過程。

 

是一種資料結構,是虛擬機器中的區域性變量表,對應物理層之上的程式資料模型。

 

區域性變量表,是一種程式執行資料模型,存放了編譯期可知的各種資料型別例如:

Boolean、byte、char、short、int、float、long、double、物件引用型別(物件記憶體地址變數,指標或控制代碼),程式執行時,根據區域性變量表分配棧幀空間大小,在執行中,大小是不變的異常型別:stackOverFlowError 執行緒請求棧深度大於虛擬機器允許深度 OutOfMemory 記憶體空間耗盡無法進行擴充套件。

 

3. 本地方法棧

 

與虛擬機器棧類似,虛擬機器棧為Java程式服務,本地方法棧支援虛擬機器的執行服務,具體實現由虛擬機器廠商決定,也會丟擲 stackOverFlowError、OutOfMemory異常。

 

4. 堆

 

是虛擬機器管理記憶體中最大的一部分,被所有執行緒共享,用於存放物件例項(物件、陣列),物理上不連續的記憶體空間,由於GC收集器,分代收集,所以劃分為:新生代 Eden、From SurVivor空間、To SurVivor空間,allot buffer(分配空間),可能會劃分出多個執行緒私有的緩衝區,老年代。

 

5. 方法區

 

與堆一樣屬於執行緒共享的記憶體區域,用於儲存虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼(動態載入OSGI)等資料。理論上屬於java虛擬機器的一部分,為了區分開來叫做 Non-Heap非堆。

 

這個區域可以選擇不進行垃圾回收,該區域回收目的主要是常量池的回收,及型別的解除安裝class,記憶體區不足時會丟擲OutOfMemory異常

 

執行時常量池:

 

方法區的一部分,Class的版本、欄位、介面、方法等,及編譯期生成的各種字面量、符號引用,編譯類載入後存放在該區域。會丟擲OutOfMemory異常。

 

6. 直接記憶體

 

直接記憶體不屬於虛擬記憶體區域,是一種基於通道與緩衝區的IO方式,可以使用本地函式直接分配堆外記憶體,在堆中儲存引用的外部記憶體地址,通過引用完成對直接引用記憶體的操作,1.4之後提供的NIO顯著提高效率,避免了堆記憶體與Native記憶體的來回複製操作,不受虛擬機器記憶體控制,會丟擲OUtOfMemory異常。

 

二:物件訪問內部實現過程

 

物件訪問 涉及到物件的地址變更狀態變更,記憶體地址移動,變數、介面、實現類、方法、父型別等。

 

1、 控制代碼方式 (訪問)

 

 

2、指標方式 (訪問)

 

 

優缺點:

 

控制代碼訪問方式:reference中儲存的是穩定的地址,物件變更時只會改變控制代碼例項資料指標,引用本身不需要修改

 

指標訪問方式:優點速度快,節省了指標定位時間開銷

 

三:記憶體區域控制引數及對應溢位異常

 

開發過程中,或程式執行過程中每次遇到OutOfMemory異常或GC異常或StackOverflowError異常我們都是一堆引數亂配,都把值調大,只是大體知道是跟jvm記憶體分配有關,具體應該怎麼調,對應的異常應該調整那些引數,或者換句話說,jvm記憶體分配區域中都分別對應那些引數大多數情況下都是不知道的,只是把相關的引數跳上去,預期結果都是應該起作用,到底能不能起作用,自己心裡也沒底。下面就來說一下jvm堆、棧、方法區等記憶體區域對應的引數,及每個區域可能丟擲的異常型別,發生異常的場景分析。

 

1、引數型別

 

  1. 堆空間引數

  2. 棧空間引數

  3. 方法區空間引數

  4. 本機直接記憶體引數

 

2、異常型別

 

1.OutOfMemory異常

2.StackOverflowError異常

 

3、輔助引數說明

 

  1. -XX:+HeapDumpOnOutOfMemoryError 列印堆記憶體異常時打印出快照資訊

  2. -XX:+HeapDumpPath 快照輸出路徑

  3. -Xmn指定eden區的大小

  4. -XX:SurvirorRation來調整倖存區的大小

  5. -XX:PretenureSizeThreshold設定進入老年代的閥值

 

4、引數說明、對應場景的異常

 

1.堆記憶體引數

 

-Xms:堆最小值(新生代和老年代之和)

-Xmx:堆最大值(新生代和老年代之和)

 

當最小值=最大值時,這時堆記憶體是不可擴充套件的。

 

例:-Xms80M -Xmx80M

 

通常將-Xmx和-Xms設定為一樣的大小來減少gc的次數,堆記憶體不足時丟擲OutOfMemoryError異常。

 

2.棧記憶體引數

 

-Xss

 

例:-Xss128k

 

單執行緒下無論棧幀太大還是棧容量太小,及引用深度超過虛擬機器允許深度都會丟擲StackOverflowError每個方法壓入棧的幀大小是不一致的。多執行緒下當每個執行緒分配棧幀太大記憶體不能夠擴充套件時丟擲OutOfMemoryError異常執行緒棧幀越大,可建立的執行緒越少。

 

3.方法區引數

 

-XX:PermSize方法區記憶體最小值

-XX:MaxPermSize 方法區記憶體最大值

 

各個執行緒共享的記憶體區域,主要用來儲存類的元資料、常量、靜態變數、即時編譯器編譯後的程式碼等資料

 

例:-XX:PermSize=20M -XX:MaxPermSize=20M

 

異常型別 OutOfMemoryError :

 

原因:常量過多,或代理反射等使用頻繁

 

4.本機直接記憶體引數

 

-XX:MaxDirectMemorySize

 

例:-XX:MaxDirectMemorySize=10M

 

不足時丟擲OutOfMemory異常

 

四:垃圾收集演算法

 

經典的垃圾回收演算法以下幾種

 

1、標記--清除演算法(Mark-Sweep)

 

回收前狀態:

 

 

回收後狀態:

 

 

優缺點:

 

演算法執行分為兩個階段標記與清除,所有的回收演算法,基本都

基於標記回收演算法做了深度優化

 

缺點:效率問題,記憶體空間碎片(不連續的空間)

 

2、複製演算法(Copying)

 

回收前狀態:

 

Eden記憶體空間 8

 

 

Survivor1空間(From空間)1

 

 

Survivor2空間(To空間) 1

 

 

Eden記憶體空間與Survivor空間 8:1

 

 

回收後狀態:

 

 

Survivor1空間(From空間)1

 

 

Eden記憶體空間與Survivor空間 8:1

 

 

優缺點:

 

比較標記清除演算法,避免了回收造成的記憶體碎片問題

 

缺點:以區域性的記憶體空間犧牲為代價,不過空間的浪費比較小,預設8:1的比例1是浪費的。

 

複製也有一定的效率與空間成本

 

3、標記整理演算法(Mark-Compact)

 

回收前狀態:

 

 

回收後狀態:

 

 

優缺點:

 

避免了,空間的浪費,與記憶體碎片問題。

 

缺點:整理時複製有效率成本。

 

五:垃圾收集器

 

1、七種垃圾收集器

 

(1) Serial(序列GC)-XX:+UseSerialGC

(2) ParNew(並行GC)-XX:+UseParNewGC

(3) Parallel Scavenge(並行回收GC)

(4) Serial Old(MSC)(序列GC)-XX:+UseSerialGC

(5) CMS(併發GC)-XX:+UseConcMarkSweepGC

(6) Parallel Old(並行GC)-XX:+UseParallelOldGC

(7) G1(JDK1.7update14才可以正式商用)

 

 

2、1~3用於年輕代垃圾回收

 

年輕代的垃圾回收稱為minor GC

 

3、4~6用於年老代垃圾回收(當然也可以用於方法區的回收)

 

年老代的垃圾回收稱為full GC

 

G1獨立完成"分代垃圾回收"

 

注意:並行與併發

 

並行:多條垃圾回收執行緒同時操作

 

併發:垃圾回收執行緒與使用者執行緒一起操作

 

4、常用五種組合

 

Serial/Serial Old

 

ParNew/Serial Old:與上邊相比,只是比年輕代多了多執行緒垃圾回收而已

ParNew/CMS:當下比較高效的組合

Parallel Scavenge/Parallel Old:自動管理的組合

G1:最先進的收集器,但是需要JDK1.7update14以上

 

5、Serial/Serial Old

 

年輕代Serial收集器採用單個GC執行緒實現"複製"演算法(包括掃描、複製)

年老代Serial Old收集器採用單個GC執行緒實現"標記-整理"演算法

Serial與Serial Old都會暫停所有使用者執行緒(即STW)

 

說明:

 

STW(stop the world):編譯程式碼時為每一個方法注入safepoint(方法中迴圈結束的點、方法執行結束的點),在暫停應用時,需要等待所有的使用者執行緒進入safepoint,之後暫停所有執行緒,然後進行垃圾回收。

 

適用場合:

 

CPU核數<2,實體記憶體<2G的機器(簡單來講,單CPU,新生代空間較小且對STW時間要求不高的情況下使用)

 

-XX:UseSerialGC:強制使用該GC組合

-XX:PrintGCApplicationStoppedTime:檢視STW時間

 

6、ParNew/Serial Old:

 

ParNew除了採用多GC執行緒來實現複製演算法以外,其他都與Serial一樣,但是此組合中的Serial Old又是一個單GC執行緒,所以該組合是一個比較尷尬的組合,在單CPU情況下沒有Serial/Serial Old速度快(因為ParNew多執行緒需要切換),在多CPU情況下又沒有之後的三種組合快(因為Serial Old是單GC執行緒),所以使用其實不多。

 

-XX:ParallelGCThreads:指定ParNew GC執行緒的數量,預設與CPU核數相同,該引數在於CMS GC組合時,也可能會用到

 

7、Parallel Scavenge/Parallel Old:

 

特點:

 

年輕代Parallel Scavenge收集器採用多個GC執行緒實現"複製"演算法(包括掃描、複製)年老代Parallel Old收集器採用多個GC執行緒實現"標記-整理"算ParallelScavenge與Parallel Old都會暫停所有使用者執行緒(即STW)

 

說明:

 

吞吐量:CPU執行程式碼時間/(CPU執行程式碼時間+GC時間)CMS主要注重STW的縮短(該時間越短,使用者體驗越好,所以主要用於處理很多的互動任務的情況)Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,說明CPU利用率越高,所以主要用於處理很多的CPU計算任務而使用者互動任務較少的情況)

 

引數設定:

 

-XX:+UseParallelOldGC:使用該GC組合

-XX:GCTimeRatio:直接設定吞吐量大小,假設設為19,則允許的最大GC時間佔總時間的1/(1+19),預設值為99,即1/(1+99)

-XX:MaxGCPauseMillis:最大GC停頓時間,該引數並非越小越好

-XX:+UseAdaptiveSizePolicy:開啟該引數,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold這些引數就不起作用了,虛擬機器會自動收集監控資訊,動態調整這些引數以提供最合適的的停頓時間或者最大的吞吐量(GC自適應調節策略),而我們需要設定的就是-Xmx,-XX:+UseParallelOldGC或-XX:GCTimeRatio兩個引數就好(當然-Xms也指定上與-Xmx相同就好)

 

注意:

 

-XX:GCTimeRatio和-XX:MaxGCPauseMillis設定一個就好

不開啟-XX:+UseAdaptiveSizePolicy,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold這些引數依舊可以配置,以resin伺服器為例

 

<jvm-arg>-Xms2048m</jvm-arg> <jvm-arg>-Xmx2048m</jvm-arg> <jvm-arg>-Xmn512m</jvm-arg> <jvm-arg>-Xss1m</jvm-arg> <jvm-arg>-XX:PermSize=256M</jvm-arg> <jvm-arg>-XX:MaxPermSize=256M</jvm-arg> <jvm-arg>-XX:SurvivorRatio=8</jvm-arg> <jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg> <jvm-arg>-XX:+UseParallelOldGC</jvm-arg> <jvm-arg>-XX:GCTimeRatio=19</jvm-arg> <jvm-arg>-XX:+PrintGCDetails</jvm-arg> <jvm-arg>-XX:+PrintGCTimeStamps</jvm-arg> View Code

 

適用場合:

 

很多的CPU計算任務而使用者互動任務較少的情況不想自己去過多的關注GC引數,想讓虛擬機器自己進行調優工作

 

8、調優方法

 

8.1 新物件預留新生代

 

由於fullGC(老年代)的成本遠比minorGC(新生代和老年代)的成本大,所以給應用分配一個合理的新生代空間,儘量將物件分配到新生代減小fullGC的頻率

 

8.2 大物件進入老年代

 

將大物件直接分配到老年代,保持新生代物件的結構的完整性,以提高GC效率, 以通過-XX:PretenureSizeThreshold設定進入老年代的閥值

 

8.3 穩定與震盪的堆大小

 

穩定的對大小是對垃圾回收有利的,方法將-Xms和-Xmx的大小一致

 

8.4 吞吐量優先

 

儘可能減少系統執行垃圾回收的總時間,故採用並行垃圾回收器

 

-XX:+UseParallelGC或使用-XX:+UseParallelOldGC

 

8.5 降低停頓

 

使用CMS回收器,同時減少fullGC的次數

 

9、獲取gc資訊的方法

 

9.1 -verbose:gc或者-XX:+PrintGC  獲取gc資訊

9.2 -XX:+PrintGCDetails  獲取更加詳細的gc資訊

9.3 -XX:+PrintGCTimeStamps  獲取GC的頻率和間隔

9.4 -XX:+PrintHeapAtGC  獲取堆的使用情況

9.5 -Xloggc:D:\gc.log  指定日誌情況的儲存路徑

 

10、jvm調優實戰-tomcat啟動加速

 

在tomcat的bin/catalina.bat檔案的開頭新增相關的配置

 

六:監控工具

 

監控工具:一般問題定位,效能調優都會使用到。

 

1、jps

 

Jps是參照Unix系統的取名規則命名的,而他的功能和ps的功能類似,可以列舉正在執行的餓虛擬機器程序並顯示虛擬機器執行的主類以及這些程序的唯一ID(LVMID,對應本機來說和PID相同),他的用法如下:

 

Jps [option] [hostid]

jps -q 只輸出LVMID

jps -m 輸出JVM啟動時傳給主類的方法

jps -l 輸出主類的全名,如果是Jar則輸出jar的路徑

jps -v 輸出JVM的啟動引數

 

2、jstat

 

jstat主要用於監控虛擬機器的各種執行狀態資訊,如類的裝載、記憶體、垃圾回收、JIT編譯器等,在沒有GUI的伺服器上,這款工具是首選的一款監控工具。其用法如下:

 

jstat [option vmid [interval [s|ms] [vount] ] ]

 

jstat 監控內容 執行緒好 重新整理時間間隔 次數

 

jstat –gc 20445 1 20 :監視Java堆,包含eden、2個survivor區、old區和永久帶區域的容量、已用空間、GC時間合計等資訊

 

jstat –gcutil 20445 1 20:監視內容與-gc相同,但輸出主要關注已使用空間佔總空間的百分比

 

jstat –class 20445 1 20:監視類的裝載、解除安裝數量以及類的裝載總空間和耗費時間等

 

.......-gccapcity......:監視內容與-gc相同,但輸出主要關注Java區域用到的最大和最小空間

.......-gccause........:與-gcutil輸出資訊相同,額外輸出導致上次GC產生的原因

.......-gcnew..........:監控新生代的GC情況

.......-gcnewcapacity..:與-gcnew監控資訊相同,輸出主要關注使用到的最大和最小空間

.......-gcold..........:監控老生代的GC情況

.......-gcoldcapacity..:與-gcold監控資訊相同,輸出主要關注使用到的最大和最小空間

.......-gcpermcapacity.:輸出永久帶用到的最大和最小空間

.......-compiler.......:輸出JIT編譯器編譯過的方法、耗時資訊

.......-printcompilation:輸出已經被JIT編譯的方法

 

3、jinfo

 

jinfo的作用是實時檢視虛擬機器的各項引數資訊jps –v可以檢視虛擬機器在啟動時被顯式指定的引數資訊,但是如果你想知道預設的一些引數資訊呢?除了去查詢對應的資料以外,jinfo就顯得很重要了。jinfo的用法如下:

 

Jinfo [option] pid

 

4、jmap

 

map用於生成堆快照(heapdump)。當然我們有很多方法可以取到對應的dump資訊,如我們通過JVM啟動時加入啟動引數 –XX:HeapDumpOnOutOfMemoryError引數,可以讓JVM在出現記憶體溢位錯誤的時候自動生成dump檔案,亦可以通過-XX:HeapDumpOnCtrlBreak引數,在執行時使用ctrl+break按鍵生成dump檔案,當然我們也可以使用kill -3 pid的方式去恐嚇JVM生成dump檔案。Jmap的作用不僅僅是為了獲取dump檔案,還可以用於查詢finalize執行佇列、Java堆和永久帶的詳細資訊,如空間使用率、垃圾回收器等。其執行格式如下:

 

Jmap [option] vmip

 

監控堆疊資訊主要用來定位問題的原因,生成堆疊快照

 

.......-dump......:生成對應的dump資訊,用法為-dump:[live,]format=b,file={fileName}

.......-finalizerinfo......:顯示在F-Queue中等待的Finalizer方法的物件(只在linux下生效)

.......-heap......:顯示堆的詳細資訊、垃圾回收器資訊、引數配置、分代詳情等

.......-histo......:顯示堆疊中的物件的統計資訊,包含類、例項數量和合計容量

.......-permstat......:以ClassLoder為統計口徑顯示永久帶的記憶體狀態

.......-F......:虛擬機器對-dump無響應時可使用這個選項強制生成dump快照

 

例子:jmap -dump:format=b,file=yhj.dump 20445

 

5、jstack

 

Jstack用於JVM當前時刻的執行緒快照,又稱threaddump檔案,它是JVM當前每一條執行緒正在執行的堆疊資訊的集合。生成執行緒快照的主要目的是為了定位執行緒出現長時間停頓的原因,如執行緒死鎖、死迴圈、請求外部時長過長導致執行緒停頓的原因。通過jstack我們就可以知道哪些程序在後臺做些什麼?在等待什麼資源等!其執行格式如下:

 

Jstack [option] vmid

 

-F 當正常輸出的請求不響應時強制輸出執行緒堆疊

-l 除堆疊資訊外,顯示關於鎖的附加資訊

-m 顯示native方法的堆疊資訊

 

6、jconsole

 

在JDK的bin目錄下,監控記憶體,thread,堆疊等

 

7、jprofile

 

類似於jconsole,比jconsole監控資訊更全面,記憶體,執行緒,包,cup 類,堆疊,等等