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、引數型別
- 堆空間引數
- 棧空間引數
- 方法區空間引數
- 本機直接記憶體引數
2、異常型別
- OutOfMemory 異常
- StackOverflowError 異常
3、輔助引數說明
- -XX:+HeapDumpOnOutOfMemoryError 列印堆記憶體異常時打印出快照資訊
- -XX:+HeapDumpPath 快照輸出路徑
- -Xmn 指定 eden 區的大小 -XX:SurvirorRation 來調整倖存區的大小
- -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 伺服器為例
-Xms2048m -Xmx2048m -Xmn512m -Xss1m -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:+UseParallelOldGC -XX:GCTimeRatio=19 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 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 檔案的開頭新增相關的配置
11、jvm 深入學習
如果想要系統深入學習 JVM 的話,我在這裡給大家推薦一個 Java 方面的交流學習群:650385180,裡面會分享一些資深架構師錄製的視訊錄影:有 Spring,MyBatis,Netty 原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM 效能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,相信對於已經工作和遇到技術瓶頸的碼友,在這個群裡會有你需要的內容。
六:監控工具
監控工具:一般問題定位,效能調優都會使用到。
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 類,堆疊,等等