【JVM调优】JVM内存管理调优浅谈
什么是JVM
Java Virtual Machine,Java虛擬機
Java虛擬機有自己完善的硬件架構,如處理器、堆棧等,還具有相應的指令系統。
Java虛擬機本質上就是一個程序,當它在命令行上啟動的時候,就開始執行保存在某字節碼文件中的指令。Java語言的可移植性正是建立在Java虛擬機的基礎上。任何平臺只要裝有針對于該平臺的Java虛擬機,字節碼文件(.class)就可以在該平臺上運行。這就是“一次編譯,多次運行”。
Java虛擬機不僅是一種跨平臺的軟件,而且是一種新的網絡計算平臺。該平臺包括許多相關的技術,如符合開放接口標準的各種API、優化技術等。Java技術使同一種應用可以運行在不同的平臺上。Java平臺可分為兩部分,即Java虛擬機(Java virtual machine,JVM)和Java API類庫。
作用:JVM是java字節碼執行的引擎,解析字節碼文件的內容,并將其翻譯為各種操作系統能理解的機器碼
運作過程:解析字節碼文件的時候,會先加載字節碼文件,將其存入java虛擬機的內存空間中,進行一系列的動作,最后運行程序得出結果
JVM內存空間
字節碼數據在java虛擬機內存中如何存放的?
虛擬機棧
用于執行java方法,每個方法執行都會創建一個棧幀,存儲局部變量表,操作數棧,動態鏈接等信息,程序執行時,棧幀入棧,執行完成后棧幀出棧
程序計數器
或者叫PC寄存器,記載著每一個線程當前運行的JAVA方法的地址,指示當前程序執行到了哪個位置:
1、執行Java方法時記錄正在執行的虛擬機字節碼指令地址;
2、執行本地方法時,計數器值為null;
每一個線程都有一個PC寄存器,也就是說PC寄存器是線程獨有的。
生命周期跟隨線程,線程啟動而產生,線程結束而消亡。
是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的行號指示器。
本地方法區
如果程序有使用到native方法,加載和執行就在這個區域。
方法區
存儲 Java 類字節碼數據的一塊區域,存儲類,常量相關的信息。
Java堆
Java虛擬機管理的內存中最大的一塊,Java虛擬機啟動的時候建立,所有線程共享,幾乎所有的對象實例都在這里分配內存。
GC主要就是在Java堆中進行的。
Java 堆根據對象存活時間的不同,Java 堆還被分為新生代(或叫 年輕代)、老年代兩個區域,新生代還被進一步劃分為 Eden 區、Survivor 0、Survivor 1 區。
新生代
老年代
GC介紹
GC,即垃圾回收
英語:Garbage Collection,縮寫為GC
GC是一種自動的內存管理機制
垃圾回收的術語:
Minor GC
從新生代空間回收內存被稱為 Minor GC,有時候也稱之為 Young GC。
Major GC
從老年代空間回收內存被稱為 Major GC,有時候也稱之為 Old GC。
Full GC
Full GC 是清理整個堆空間 —— 包括新生代、老年代。因此 Full GC 可以說是 Minor GC 和 Major GC 的結合。
Stop-The-World
翻譯為全世界暫停,簡稱 STW,是指在進行垃圾回收時,因為標記或清理的需要,必須讓所有執行任務的線程停止執行任務,從而讓垃圾回收線程完成回收垃圾的時間。
垃圾回收機制的重點:
哪些內存需要回收?
垃圾收集器會對堆進行回收前,確定對象中哪些是“存活”,哪些是“死亡”(不可能再被任何途徑使用的對象);
當一個對象到GC Roots沒有任何引用鏈相連,即不可達時,則證明此對象時不可用的。
舉例:一顆樹有很多丫枝,其中一個分支斷了,跟樹上沒有任何聯系,那就說明這個分支沒有用了,就可以當垃圾回收去燒了。
什么時候回收?
主要的場景:
(1)JVM 無法為一個新的對象分配空間時會觸發 Minor GC
(2)老年代空間不夠,那么觸發 Major GC
(3)當準備要觸發一次Minor GC時,如果出現歷史Minor GC的平均晉升大小比目前 “老年代”剩余的空間大,老年代剩余空間不夠用于新生代晉升,則不會觸發young GC,而是轉為觸發full GC
如何回收?
垃圾收集算法:
1)標記—清除算法
標記—清除算法(Mark-Sweep),是最基礎的收集算法,它分為“標記”和“清除”兩個階段:首先標記出所需回收的對象,在標記完成后統一回收掉所有被標記的對象,它的標記過程是通過可達性分析算法實現的。
回收前狀態
回收后狀態
算法缺點:
標記和清除過程的效率都不高;標記清除后會產生大量不連續的內存碎片。
2)復制算法
復制算法將可用內存按容量分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活著的對象復制到另外一塊內存上面,然后再把已使用過的內存空間一次清理掉。
回收前狀態
回收后狀態
復制算法優點:
每次只對一塊內存進行回收,運行高效;大大減少了內存碎片的出現;
缺點:
可初始分配的最大內存縮小了一半;
3)標記—整理算法
分為“標記”、“整理”、“清除”三個階段
先對內存區域的對象進行標記,區分出“存活對象”和“可回收對象”,
讓所有的對象都向內存區域一端移動,直接清理掉可回收的對象;
回收前狀態
回收后狀態
4)分代回收
根據對象的存活周期的不同,將內存劃分為新生代和老年代。
在新生代中,每次垃圾收集時都會發現有大量對象死去,只有少量存活,因此可選用復制算法來完成收集;
新生代中的對象98%都是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊比較大的Eden空間和兩塊較小的Survivor空間(8:1:1的比例),每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。
每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的空間會被預留。
老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就使用標記—清除算法或標記—整理算法來進行回收
新生代回收
JVM常用參數
Jvm啟動參數共分為三類:
一、是標準參數(-),所有的JVM實現都必須實現這些參數的功能,而且向后兼容;
二、是非標準參數(-X),默認jvm實現這些參數的功能,但是并不保證所有jvm實現都滿足,且不保證向后兼容;
三、是非Stable參數(-XX),此類參數各個jvm實現會有所不同;
-Xms256m 為jvm啟動時分配的內存-Xmx256m 為jvm運行過程中分配的最大內存,建議和Xms保持一致-Xmn128m 設置年輕代大小為128M 此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。-Xss256k 為jvm啟動的每個線程分配的內存大小-XX:NewSize 和-XX:MaxNewSize 用于設置年輕代的大小,建議設為整個堆大小的1/3或者1/4,兩個值設為一樣大。-XX:PermSize=1024M 和 -XX:MaxPermSize=1024M JVM初始分配的非堆內存, 不會被回收, 建議與maxPermSize相同-XX:SurvivorRatio=4 年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。 設置為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/6-XX:NewRatio=4 設置年輕代(EC+S0C+S1C)和年老代(OC)的比值。如:為4,表示年輕代與年老代比值為1:4,年輕代占整個年輕代年老代和的1/5-XX:InitialTenuringThreshol 和-XX:MaxTenuringThreshold 用于設置晉升到老年代的對象年齡的最小值和最大值,每個對象在堅持過一次Minor GC之后,年齡就加1。-XX:+PrintGC 輸出GC日志 -XX:+PrintGCDetails 輸出GC的詳細日志 -XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式) -XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800) -Xloggc:../logs/gc.log 日志文件的輸出路徑-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M JVM的一個日志文件達到了20M以后,就會寫入另一個新的文件,最多會有5個日志文件,他們的名字分別是:gc.log.0、gc.log.1 等 -XX:+PrintHeapAtGC 在進行GC的前后打印出堆的信息-XX:+PrintTenuringDistribution 這個參數用于顯示每次Minor GC時Survivor區中各個年齡段的對象的大小。2020-06-29T03:45:25.512+0800: 76.642: [GC pause (G1 Evacuation Pause) (young) Desired survivor size 402653184 bytes, new threshold 2 (max 15) - age 1: 169351248 bytes, 169351248 total - age 2: 331023392 bytes, 500374640 total - age 3: 64201192 bytes, 564575832 total , 1.2957512 secs]-XX:MaxTenuringThreshold 用于調整對象晉升老年代的所需經歷的GC次數,默認15次,即在年輕代的對象經過了指定次數的 GC 后,將在下次 GC 時進入老年代。JVM調優淺談
JVM內存管理很差會出現什么情況?
1,內存溢出
2,頻繁Full GC
….
內存溢出:
OutOfMemoryError: Java heap space 堆溢出
最常見的內存溢出,當在JVM中如果98%的時間是用于GC且可用的 Heap size 不足2%的時候將拋出此異常信息。
優化建議:
Heap Size 最大不要超過可用物理內存的80%,一般的要將-Xms和-Xmx選項設置為相同,而-Xmn為3/8的-Xmx值。
OutOfMemoryError: PermGen space 非堆溢出(永久保存區域溢出)
這塊內存主要是被JVM存放Class和Meta信息的,是 JVM初始分配的非堆內存,沒有GC回收,一般發生在程序的啟動階段。
優化建議:
通過-XX:PermSize和 -XX:MaxPermSize設置合適的內存大小
OutOfMemoryError: unable to create new native thread. 無法創建新的線程
常見在高并發的業務場景,線程池一直新建線程,每個線程都有自己的Stack Space,Stack Space的空間是獨立分配的,當超出jvm的棧內存大小時, 就會報出無法再創建線程的錯誤.
優化建議:
設置合適的-Xss參數,優化線程池資源分配。
java.lang.StackOverflowError : Thread Stack space
棧溢出了,常見于代碼遞歸的層次過多。
優化建議:
修改程序、設置合適的-Xss參數。
MinorGC 日志
2020-06-23T04:35:59.604+0800: 463.092: [GC (Allocation Failure) 2020-06-23T04:35:59.604+0800: 463.092: [ParNew: 43296K->7006K(47808K), 0.0136826 secs] 44992K->8702K(252608K), 0.0137904 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 2020-06-23T04:35:59.604+0800 日志文件的時間戳463.092 JVM記錄的時間戳:GC開始,相對JVM啟動的相對時間,單位是秒GC 觸發了YoungGC/MinorGCAllocation Failure – MinorGC的原因,由于年輕代不滿足申請的空間,因此觸發了MinorGC[ParNew(使用ParNew作為新生代的垃圾回收器,采用的是復制算法): 43296K(年輕代垃圾回收前的大小)->7006K(年輕代垃圾回收以后的大小)(47808K) (年輕代的總大小), 0.0136826 secs(回收時間)] 44992K(堆區垃圾回收前的大小)->8702K(堆區垃圾回收后的大小)(252608K)(堆區總大小), 0.0137904 secs(回收時間)][Times: user=0.03 sys=0.00, real=0.02 secs] user:GC 線程在垃圾收集期間所使用的 CPU 總時間; sys:系統調用或者等待系統事件花費的時間; real:應用被暫停的時鐘時間,由于 GC 線程是多線程的,導致了 real 小于 (user+real),如果是 gc 線程是單線程的話,real 是接近于 (user+real) 時間。FullGC 日志
2018-04-12T13:48:26.233+0800: 15578.148: [GC [1 CMS-initial-mark: 6294851K(20971520K)] 6354687K(24746432K), 0.0466580 secs] [Times: user=0.04 sys=0.00, real=0.04 secs] 2018-04-12T13:48:26.280+0800: 15578.195: [CMS-concurrent-mark-start] 2018-04-12T13:48:26.418+0800: 15578.333: [CMS-concurrent-mark: 0.138/0.138 secs] [Times: user=1.01 sys=0.21, real=0.14 secs] 2018-04-12T13:48:26.418+0800: 15578.334: [CMS-concurrent-preclean-start] 2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-preclean: 0.056/0.057 secs] [Times: user=0.20 sys=0.12, real=0.06 secs] 2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-abortable-preclean-start] 2018-04-12T13:48:29.989+0800: 15581.905: [CMS-concurrent-abortable-preclean: 3.506/3.514 secs] [Times: user=11.93 sys=6.77, real=3.51 secs] 2018-04-12T13:48:29.991+0800: 15581.906: [GC[YG occupancy: 1805641 K (3774912 K)] 2018-04-12T13:48:29.991+0800: 15581.906: [GC2018-04-12T13:48:29.991+0800: 15581.906: [ParNew: 1805641K->48395K(3774912K), 0.0826620 secs] 8100493K->6348225K(24746432K), 0.0829480 secs] [Times: user=0.81 sys=0.00, real=0.09 secs]2018-04-12T13:48:30.074+0800: 15581.989: [Rescan (parallel) , 0.0429390 secs]2018-04-12T13:48:30.117+0800: 15582.032: [weak refs processing, 0.0027800 secs]2018-04-12T13:48:30.119+0800: 15582.035: [class unloading, 0.0033120 secs]2018-04-12T13:48:30.123+0800: 15582.038: [scrub symbol table, 0.0016780 secs]2018-04-12T13:48:30.124+0800: 15582.040: [scrub string table, 0.0004780 secs] [1 CMS-remark: 6299829K(20971520K)] 6348225K(24746432K), 0.1365130 secs] [Times: user=1.24 sys=0.00, real=0.14 secs] 2018-04-12T13:48:30.128+0800: 15582.043: [CMS-concurrent-sweep-start] 2018-04-12T13:48:38.412+0800: 15590.327: [CMS-concurrent-sweep: 8.193/8.284 secs] [Times: user=30.34 sys=16.44, real=8.28 secs] 2018-04-12T13:48:38.419+0800: 15590.334: [CMS-concurrent-reset-start] 2018-04-12T13:48:38.462+0800: 15590.377: [CMS-concurrent-reset: 0.044/0.044 secs] [Times: user=0.15 sys=0.10, real=0.04 secs]階段1:Initial Mark
2018-04-12T13:48:26.233+0800: 15578.148: [GC [1 CMS-initial-mark: 6294851K(20971520K)] 6354687K(24746432K), 0.0466580 secs] [Times: user=0.04 sys=0.00, real=0.04 secs]CMS-initial-mark:初始標記階段,CMS是老年代垃圾回收器,基于標記-清除算法實現, 它會收集所有 GC Roots 以及其直接引用的對象; 6294851K:當前老年代使用的容量,這里是 6G; (20971520K):老年代可用的最大容量,這里是 20G; 6354687K:整個堆目前使用的容量,這里是 6.06G; (24746432K):堆可用的容量,這里是 23.6G; 0.0466580 secs:這個階段的持續時間;這個是 CMS 兩次 stop-the-wolrd 事件的其中一次,這個階段的目標是:標記那些直接被 GC root 引用 或者被年輕代存活對象所引用的所有對象階段2:CMS-concurrent-mark
2018-04-12T13:48:26.280+0800: 15578.195: [CMS-concurrent-mark-start] 2018-04-12T13:48:26.418+0800: 15578.333: [CMS-concurrent-mark: 0.138/0.138 secs] [Times: user=1.01 sys=0.21, real=0.14 secs]CMS-concurrent-mark:并發標記階段,遍歷老年代,標記所有存活的對象,由第一階段標記過的對象出發, 所有可達的對象都在本階段標記; 0.138/0.138 secs:這個階段的持續時間與時鐘時間;2018-04-12T13:48:26.418+0800: 15578.334: [CMS-concurrent-preclean-start] 2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-preclean: 0.056/0.057 secs] [Times: user=0.20 sys=0.12, real=0.06 secs]階段3:Concurrent Preclean
Concurrent Preclean :并發預清理階段,對在前面并發標記階段中引用發生變化的對象進行標記, 包含:從新生代晉升、新分配、被更新的對象,并發地重新掃描這些對象; 0.056/0.057 secs:這個階段的持續時間與時鐘時間;2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-abortable-preclean-start] 2018-04-12T13:48:29.989+0800: 15581.905: [CMS-concurrent-abortable-preclean: 3.506/3.514 secs] [Times: user=11.93 sys=6.77, real=3.51 secs]階段4:Concurrent Abortable Preclean
Concurrent Abortable Preclean :并發可中止的預清理階段,和上一階段工作內容一樣,但可以控制結束 時間,這個階段持續時間依賴于很多的因素:完成的工作量、掃描持續時間等;2018-04-12T13:48:29.991+0800: 15581.906: [GC[YG occupancy: 1805641 K (3774912 K)] 2018-04-12T13:48:29.991+0800: 15581.906: [GC2018-04-12T13:48:29.991+0800: 15581.906: [ParNew: 1805641K->48395K(3774912K), 0.0826620 secs] 8100493K->6348225K(24746432K), 0.0829480 secs] [Times: user=0.81 sys=0.00, real=0.09 secs] 2018-04-12T13:48:30.074+0800: 15581.989: [Rescan (parallel) , 0.0429390 secs] 2018-04-12T13:48:30.117+0800: 15582.032: [weak refs processing, 0.0027800 secs] 2018-04-12T13:48:30.119+0800: 15582.035: [class unloading, 0.0033120 secs] 2018-04-12T13:48:30.123+0800: 15582.038: [scrub symbol table, 0.0016780 secs] 2018-04-12T13:48:30.124+0800: 15582.040: [scrub string table, 0.0004780 secs] [1 CMS-remark: 6299829K(20971520K)] 6348225K(24746432K), 0.1365130 secs] [Times: user=1.24 sys=0.00, real=0.14 secs]階段5:remark
remark 重標記階段重標記階段(CMS的第二個STW階段),暫停所有用戶線程,從GC Root開始重新掃描整堆,標記存活的對象。 雖然CMS只回收老年代的垃圾對象,但是這個階段依然需要掃描新生代,因為很多GC Root都在新生代, 而這些GC Root指向的對象又在老年代,這稱為“跨代引用”。YG occupancy: 1805641 K (3774912 K):年輕代當前占用量及總容量,這里分別是 1.71G 和 3.6G; ParNew:觸發了一次 young GC,原因是為了減少年輕代的存活對象,盡量使年輕代更干凈一些; [Rescan (parallel) , 0.0429390 secs]:這個 Rescan 是當應用暫停的情況下完成對所有存活對象的標記, 這個階段是并行處理的,這里花費了 0.0429390s; [weak refs processing, 0.0027800 secs]:第一個子階段,它的工作是處理弱引用; [class unloading, 0.0033120 secs]:第二個子階段,它的工作是:unloading the unused classes; [scrub symbol table, 0.0016780 secs]、[scrub string table, 0.0004780 secs]: 最后一個子階段,它的目的是:cleaning up symbol and string tables which hold class-level metadata and internalized string respectively CMS-remark:remark結束,輸出當前老年代的使用量與總量6299829K(20971520K), 堆的使用量與總量6348225K(24746432K)2018-04-12T13:48:30.128+0800: 15582.043: [CMS-concurrent-sweep-start] 2018-04-12T13:48:38.412+0800: 15590.327: [CMS-concurrent-sweep: 8.193/8.284 secs] [Times: user=30.34 sys=16.44, real=8.28 secs]階段6:并發清理階段
這個階段主要是清除那些沒有被標記的對象,回收它們的占用空間;這里不需要 STW, 它是與用戶的應用程序并發運行。2018-04-12T13:48:38.419+0800: 15590.334: [CMS-concurrent-reset-start] 2018-04-12T13:48:38.462+0800: 15590.377: [CMS-concurrent-reset: 0.044/0.044 secs] [Times: user=0.15 sys=0.10, real=0.04 secs]階段7:Concurrent Reset階段
這個階段也是并發執行的,它會重設 CMS 內部的數據結構,為下次的 GC 做準備。Full GC頻繁發生怎么辦?
排查方向:
1、根據FULLGC日志分析,消耗在什么階段;
2、JVM參數設置不合理;
3、是否有內存泄露;
4、對象的內存分配存在問題,大對象過多;
5、使用jstat、jstack、jmap命令定位;
總結
以上是生活随笔為你收集整理的【JVM调优】JVM内存管理调优浅谈的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【HTTP协议】HTTP状态码列表大全
- 下一篇: 【面试题】Spring,SpringMV