JVM & GC
運行時數據區
- 首先弄清楚一點:一個java進程開啓一個jvm,進程又可分成多線程運行
- 進程在 jvm 上運行時,數據保存在運行時數據區,運行時數據區包括堆、方法棧、虛擬機棧、本地方法區和程序計數器,其中堆和方法棧是線程共享的,虛擬機棧、本地方法區和程序計數器是線程私有的
堆(線程共享)
- 存儲程序運行時用到的所有對象,而線程虛擬機棧的局部變量表保存的是對象的引用指針;堆空間分爲新生代、老年代、元數據(jdk1.8之後,1.7之前稱爲永久代),元數據實際上不屬於堆空間
爲什麼分代? 對象的生命週期不一樣,GC過後有些被回收有些存活下來,存活下來的對象也不必與新來的對象一起GC,需要另外空間來存放存活下來的對象
爲什麼Eden:servivor是8:1:1而不是9:1? Minor GC => 新生代 Major GC => 老年代 一次 Major GC 所花時間 > 一次 Minor GC 所花時間 所以我們希望新生代的對象儘量不要被放到老年代,即 GC
之後應有98%的對象被回收,如果是9:1的話,對象很容易存活到老年代,所以希望對象在新生代進行更多次的GC以達到目標對象分配比例配置:-XX:SurvivorRatio=8
新生代
- 新生代佔堆空間的1/3,分爲Eden區(8/10)、From區(1/10)、To區(1/10)
- 新創建的對象會被放入Eden區,當Eden區空間不足時,虛擬機會做一次 Minor GC,大部分對象會被垃圾回收器回收,剩下的存活的對象放入 From 區,這是複製回收算法
- 當 From區滿時,進行 Minor GC 會對 From 區也做 GC,存活的會進入 To 區,此時 From 區與 To 區角色互換
- 當 Eden 區再次滿的時候, Minor GC 會把存活下來的對象放入 To 區(因爲From 區與 To 區已經角色互換)
老年代
- 當 Minor GC之後 新生代滿或放不下新對象時,會觸發擔保機制,把存活的對象放入老年區
- 當整個堆空間放滿之後,會進行 Full GC,Full GC = Minor GC + Major GC,System.gc() 引起的是 Full GC
元空間
- JDK1.7之前: 永久代
- JDK1.8之後: 元空間(直接內存),這樣設計是爲了解決永久代可能溢出的問題,可以動態擴容,屬於堆外內存,可能會擠壓堆內內存,所以根據需求定義其大小
堆空間大小的設定
通過GC日誌獲取多次 full GC 存活下來的活躍數據的平均值
總堆大小 = 活躍數據 * 4
新生代 = 活躍數據 * 1.5
老年代 = 總堆大小 - 新生代
(虛擬機有默認值和自定調整)
內存規整
當多線程進行對Eden區存放堆數據的時候會出現線程安全問題(指針碰撞)
棧上分配:線程在Eden區有自己的Buffere(可調整),可避免過多的鎖,當Buffere不足時會清除到堆內存
堆的調整
-Xms …m 堆的起始大小(start)
-Xmm …m 堆的最大值(max)
-Xmn …m 堆的新生代大小(new)
-Xss …m 每個線程的棧大小
方法區(線程共享)
線程共享,保存類信息、靜態變量、常量(jdk1.7)
程序計數器(線程私有)
- 指向當前線程所執行的jvm指令的地址
- 當線程掛起時,可以保存線程運行的狀態,以便下次繼續執行
虛擬機棧(線程私有)
- 線程私有,保存當前線程運行方法所需要的數據、指令和返回地址,一個方法對應一個棧幀,每調用一個方法會壓入一個棧幀,方法執行完出棧
- 每一個棧幀包含局部變量表、操作數棧、動態鏈接、方法出口等
局部變量表
保存當前線程執行的方法的局部變量,寬度爲4字節(32位),當線程要對變量操作時,會從棧頂的局部變量表開始找,找不到再往下找
操作數棧
- 保存線程執行指令相關的操作數
- 操作數棧和局部變量表是緊密聯繫的,例如 int c = a + b 在虛擬機底層執行的指令:
iload_1
iload_2
iadd
istore_3
動態鏈接
保存線程方法運行時引用的類庫方法的接口地址
方法出口
方法執行完成的返回地址,正常返回:return,異常:執行異常處理
本地方法棧(線程私有)
線程私有,保存nactive方法,底層用c/c++實現的、沒有實現類的方法
GC-垃圾回收
垃圾回收機制是針對堆空間中存放的對象數據的, 進行垃圾回收可以在一定程度上節省內存空間,以便放入更多的數據
判斷算法
引用計數法
給每一個對象增加一個被應用計數標誌以判斷該對象是否可被清楚,但可能會出現循環引用,所以JVM不用這個算法
可達性分析
GC Root:
- 虛擬機棧中局部變量表引用的對象
- 方法區中靜態變量和常量引用的對象
- 本地方法棧中JNI引用的對象
當帶對象節點不可達時,會進入finalize()方法,不一定會立即被回收,看有沒有重寫finalize()方法
回收算法
標記-清除算法
對不可達的對象進行標記和清除,但是會產生內存碎片
複製-回收算法
對不可達對象進行回收,存活下來的對象放入servivor區
標記-整理算法
對不可達對象進行標記、清除和整理,因爲進行了在 GC 的時候整理所以不會產生內存碎片
垃圾集收器
- 雖然每個進程開啓一個 jvm,但同一個 jdk 默認垃圾集收器相同
- 新生代的垃圾集收器:serial / parNew / parallel 都用的是複製回收算法
- 老年代的垃圾集收器:CMS(標記 - 清除) / serial Old(標記 - 整理) / parallel Old(標記 - 整理)
新生代(用複製回收算法)
serial
單線程執行垃圾回收代碼,垃圾回收時其他線程不能執行,即應用線程停頓(stop-the-world)
parNew
多線程執行垃圾回收代碼,垃圾回收時其他線程不能執行,在多核心CPU的情況下停頓時間會比 serial 少
配置線程個數:-XX:parallelGCThreads=n
parallel
主要關注吞吐量,吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾回收時間)
-XX:MaxGCPauseMillise=n // 控制GC停止時間
-XX:GCTimeRatio=n // 控制GC運行時間的比率
-XX:UseAdaptivesizePolicy // 開啓全局維護策略
老年代
CMS (用標記 - 清除算法)
- 主要關注減小回收停頓時間(stop-the-world),但增加了回收時間,這是 CMS 與 parallel的區別
- 先進行GC Root可達標記
- 再進行併發標記,在併發標記的同時業務線程能併發運行
- 接下來 stop-the-world 重新整理,因爲併發標記的過程用戶線程也在執行
- 接着進行併發清除
-XX:CMSInititiongOccupancyFraction // 碎片整理
-XX:+UseCMSCompactAtFullCollection // Full GC 時開啓壓縮整理
-XX:CMSFullGCsBeforeCompaction // 設置多少次 Full GC 之後開啓壓縮
serial Old (用標記 - 整理算法)
CMS備用預案,觸發擔保機制後老年代空間不足時,會進行 Full GC
parallel Old (用標記 - 整理算法)
查看當前JVM的垃圾集收器
-XX:+PrintFlagsFinal
-XX:+PrintCommandLineFlags
GC 日誌
輸出日誌
-Xloggc: /…gc.log
分析 gc.log 的信息,例如:
解讀:[GC類型 GC前佔用空間 -> GC後佔用空間(總空間), GC耗時]
-XX: +PrintGCTimeStamps
-XX: +PrintGCDetails
日誌文件控制
-XX: -UseGCLogFileRotation
-XX: GCLogFileSize=8k
監視日誌文件命令
tail -f gc.log // GC日誌
tail -f catalina.log // 應用日誌
JDK自帶監控工具
jconsole // java 監視和管理控制檯
jps // 拿到pid
jmap -heap pid // heap使用情況
jstat -gcutil pid 毫秒 // GC數據統計(Old GC沒有變化且還在增長 => 內存泄漏)
jstat -gccause pid 毫秒 // 引發GC的原因
jvisualVM // 查看線程運行狀況和dump等
jstack pid > p.txt // 導出線程的dump
MAT
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/…error.hprof
- 查找內存泄漏原因:
- GC日誌: xxx -> xxx(xxx) 有沒有回收
- MAT: Retained Heap 和 GC Root 指向