深入理解JVM
- 第二章——JVM內存模型
- 第三章——GC算法和收集器
- 第四章——調優(yōu)工具
- 第七章——類加載
1. JVM內存模型:堆、棧、程序計數(shù)器、方法區(qū)
- 堆:是jvm中最大的一塊內存空間,堆主要存放用new創(chuàng)建的對象,gc回收主要回收的就是堆中的對象空間。堆又可以分為年輕代和老年代
- 年輕代用于存放新生對象和短期存活的對象。年輕代又可以分為eden區(qū)、FromSurvivor區(qū)和ToSurvivor區(qū)。對象優(yōu)先分配到eden區(qū),當eden區(qū)空間滿了之后,會執(zhí)行MirrorGc(復制算法),回收年輕代的空間。如果對象經(jīng)過多次gc依舊存活(默認15次),會移動到老年代
- 老年代主要存放長期存活對象和大對象。如果是大對象,直接分配到老年代。當老年代空間滿了之后,會執(zhí)行FullGc,回收堆空間,如果FullGc之后空間依舊不足,拋出OOM
- 棧:分為java虛擬機棧和本地方法棧。
- java虛擬機棧執(zhí)行java方法,創(chuàng)建棧幀,存放著局部變量表,局部變量表上存放著基本數(shù)據(jù)類型和引用數(shù)據(jù)類型的引用指針。java虛擬機棧用于執(zhí)行線程。可能導致OOM。
- 本地方法棧執(zhí)行System方法
- java虛擬機??赡艹霈F(xiàn)棧溢出,因為每次遞歸產生的信息都會存放在java虛擬機棧
- 程序計數(shù)器:每個線程都有一個程序計數(shù)器,是當前線程在所執(zhí)行的字節(jié)碼的行號指示器。單線程下可有可無,多線程中CPU通過時間片輪轉調度方式來調用線程,發(fā)生中斷時,當前線程在字節(jié)碼執(zhí)行的位置會被保存,當線程再次獲得時間片時,從保存的位置繼續(xù)執(zhí)行,實現(xiàn)快速恢復狀態(tài)。如果是java方法,記錄字節(jié)碼文件的行號,如果是本地方法,值為0
- 方法區(qū):用于存放類信息,常量、靜態(tài)變量和編譯后的代碼,1.8之前用永久代實現(xiàn),容易發(fā)生OOM。1.8之后被元空間替代,元空間是本地內存??梢酝ㄟ^-XX:MetaSpaceSize設置元空間大小。
- 方法區(qū)也會參與GC回收,但條件苛刻,會進行常量池回收和類卸載
- 當堆中不存在該類的實例
- 加載該類的類加載器被回收
- class對象以及被回收
- 方法區(qū)也會參與GC回收,但條件苛刻,會進行常量池回收和類卸載
- 運行時常量池:屬于方法區(qū)
2. 哪些是線程共享的,哪些是線程私有的
- 線程共享:堆、方法。因為存放著對象和常量、靜態(tài)變量。
- 線程私有:棧、程序計數(shù)器。
3. 哪些區(qū)域可能發(fā)生OOM:堆、棧、方法區(qū)
- 堆:
- 老年代存在大對象,導致無法繼續(xù)存入對象。可以通過jmap查看進程的堆內存使用情況。如果是存在大對象,因為對象必須要存活著,所以可以通過-Xmx和-Xms調整堆內存大小
- 存在未被回收的引用,發(fā)生內存泄漏。可以通過可達性分析查找泄漏對象,通過Gcroots查找
- 棧
- 大量創(chuàng)建線程,導致java虛擬機棧內存不足??梢酝ㄟ^-Xss調整棧大小。
- 如果棧遞歸深度過深,會發(fā)生棧溢出
- 方法區(qū)
- 運行時常量池存放大量的String
- 運行時創(chuàng)建大量的類,存放大量類信息到方法區(qū)
- 1.8之前,永久代和方法區(qū)綁定,可以通過調整永久代的大小解決:-XX:MaxPermSize
- 1.8之后,方法區(qū)被元空間替代,通過調用元空間大小解決:-XX:MetaSpaceSize
4. 各種變量在JVM的位置:局部變量、全局變量、靜態(tài)變量
- 局部變量:基本數(shù)據(jù)類型變量和值都在棧。引用數(shù)據(jù)類型的變量在棧,值在堆
- 全局變量:變量和值都在堆中
- 靜態(tài)變量、常量:方法區(qū)
5. 類加載過程:加載、連接、初始化
- 加載:將字節(jié)碼文件加載到jvm,將類的靜態(tài)變量、靜態(tài)方法、常量和編譯后的代碼存入方法區(qū),生成一個class對象
- 連接
- 驗證:檢查字節(jié)碼文件符合Java虛擬機規(guī)范,確保加載后不會發(fā)生錯誤
- 準備:在方法區(qū)中為靜態(tài)變量分配內存空間并設初值為0
- 解析:將常量的符號引用轉化未直接引用。"a"會被替換為內存地址
- 初始化:為靜態(tài)變量賦值
6. 類加載器:
- BootstrapClassLoader:啟動類加載器。加載jre中的rt.jar
- ExtensionClassLoader:拓展類加載器。加載lib中的ext文件
- ApplicationClassLoader:應用類加載器。加載環(huán)境變量classpath和java.class.path的類
7. 雙親委派機制,如何破壞,全盤委托
- 流程:當某個類加載器接到加載任務時,先檢查該類是否被加載,如果已經(jīng)被加載,返回class對象,否則將加載任務交給父類加載器進行加載。當父類加載器無法加載類時,才會由子類加載器進行加載
- 優(yōu)點:避免類重復加載
- 破壞雙親委派:重寫ClassLoader類的loadClass()方法
- 不想破壞雙親委派:重寫ClassLoader類的findClass()方法
- 全盤委托:當ClassLoaderA加載一個類時,如果沒有指定使用ClassLoaderB加載類的相關依賴類,那么這個類的相關依賴類也會由ClassLoaderA進行加載
8. Java引用類型,GC標記方法
強引用:不會被GC回收,new創(chuàng)建的對象是強引用
弱引用:一定會被GC
虛引用:隨時會被GC
軟引用:內存不足時會被GC
-
引用計數(shù)法:每個對象都有一個引用計數(shù)器,有引用計數(shù)+1,釋放引用計數(shù)-1。0表示可以回收。
- 存在循環(huán)引用問題:如果兩個對象相互引用,那么它們的計數(shù)都不為0,無法被回收
- 優(yōu)點:實時計算,幾乎沒有延遲
- 缺點:實時計算,開銷大,吞吐量下降
-
可達性分析:從Gcroots開始往下搜索,當一個對象到gcroots中沒有任何一條引用鏈與之相連,表明該引用可以被回收。
- GCroot引用鏈類似樹結構,GcRoot會與需要存活的對象相連,而可以被回收的對象則不會相連
- 對象會經(jīng)歷兩次標記才能被確定為可回收。第二次標記通過finalize()方法判斷
Gcroots:java虛擬機棧的(棧幀的局部變量表)引用對象,本地方法棧的引用,方法區(qū)的靜態(tài)變量引用,方法區(qū)的常量引用
9. GC算法
- 標記清除算法
- 過程:先對需要存活的對象進行標記,標記完成之后清除沒有被標記的對象
- 問題:如果堆中存在大量對象,標記和清除效率低,清除過程會產生空間碎片
- 標記復制算法
- 過程:先將內存分成大小相等的兩塊,先使用其中一塊,當這一塊空間滿了之后,先堆需要存活的對象進行標記,然后清除沒有被標記的對象,將存活的對象移到另一快
- 問題:運行高效,空間浪費
- 標記整理算法
- 過程:先對需要存活的對象進行標記,標記完成后將標記的對象移到一邊,然后清除邊界以外的空間
- 優(yōu)點:不會產生空間碎片
- 缺點:用于老年代,需要標記的對象多且移動開銷大,效率低
- 分代收集算法:目前hot spot虛擬機使用分代收集算法管理堆空間。分為老年代和新生代。
- 新生代:朝生夕死,存活率低,使用復制算法
- 老年代:長期存活與大對象,存活率高,使用標記整理算法
10. 為什么復制算法比標記整理要快,為什么老年代不用復制算法
- 因為復制算法是用于年輕代,年輕代對象大多存活時間都比較短且都比較小,所以標記的對象少而且占用空間小,因此復制算法可以將內存分成兩塊也不用擔心放不下。且移動對象的開銷也小。因此復制算法的運行速度快
- 因為老年代的對象大多都是長期存活且大對象,如果使用復制算法,內存開銷大,再者就算標記的過程也會慢,因為老年代的對象大多都是要存活的,并且老年代存在大對象,復制移動的開銷也大。
11. GC收集器
- CMS:并發(fā)標記清除
- 過程:初始標記、并發(fā)標記、重新標記、并發(fā)清除
- 初始標記和重新標記會發(fā)生STW,但因為是并發(fā),停頓短
- 優(yōu)點:并發(fā)標記清除,效率高,停頓短
- 缺點:
- 和用戶線程并發(fā)執(zhí)行,會占用一部分線程,程序變慢
- 基于標記清楚算法,會產生空間碎片
- 使用:-XX:UseConcMarkSweep=true
- G1:并行并發(fā)不分代,將堆內存劃分為大小相等的Region區(qū)域,進行回收,基于標記整理
- 過程:初始標記(簡單標記一下)、并發(fā)標記(可達性分析)、最終標記(STW)、篩選清除
- 初始標記和最終標記會發(fā)生STW,但因為是并行并發(fā),停頓短
- 優(yōu)點:并行并發(fā)效率高,基于標記整理,不會產生空間碎片
12. 為什么會發(fā)生STW
- 在進行gc時,需要移動對象(比如復制算法移動對象),進而會導致對象引用發(fā)生更新,為了保證引用更新的正確性,需要在進行gc時暫停其他所有線程。
- gc在進行回收垃圾時,其他線程要停止才能清除干凈,要是一邊清除以便產生垃圾,會影響gc的效率和gc的負擔。尤其是在進行標記時
13. 為什么要進行分代GC
- 堆對中存活時間不同的對象采用不同的gc策略,管理起來更高效
- 對于年輕代,對象大多存活時間比較短且對象小,一般采用復制算法,存活對象少且空間小,使用較小的內存開銷和移動開銷就能進行管理
- 對于老年代,對象存活時間長且對象大,不適合采用復制算法。采用標記整理算法。
14. JVM參數(shù)
- -Xms:堆大小,1/64
- -Xmx:最大堆大小,1/4
- -XX:NewRatio:年輕代和老年代比值:1:2
- -XX:SurvivorRatio:eden區(qū)、FS區(qū)、TS區(qū)比值:8:1:1
- -Xss:線程大小
- -XX:MetaSpaceSize:元空間大小
- -XX:MaxTeruningThreashold:年輕代對象最大存活次數(shù),默認15次
15. JVM內存分配原則和空間擔保機制
- 內存分配原則
- 對象優(yōu)先分配到年輕代的eden區(qū)
- 大對象直接進入老年代
- 年輕代中長期存活對象進入老年代
- 空間擔保機制:在年輕代進行MirrorGc之前,先檢查老年代的內存是否足夠存放年輕代的所有對象,如果不夠,老年代進行FullGc
16. FullGc觸發(fā)
- System.gc
- 老年代空間不足
- 空間擔保機制
17. 如果頻繁觸發(fā)FullGc要怎么辦
- 通過jmap下載dump文件(前提要開啟headDumpBeforeFullGc),通過visualVM導入dump文件進行分析
jmap -dump:format=b,file=xxx.dump [進程id]
18. System.gc一定會執(zhí)行嗎
- 不一定會執(zhí)行,jvm會記下這個請求,但是不一定不執(zhí)行,當System.runFinallization()返回true時,表明可以執(zhí)行System.gc()。
System.gc();
runtime.runFinalizationSync();
System.gc();
19. jvm故障處理工具:jps、jmap、jstack
- jps:查看虛擬機進程,列出當前正在運行的虛擬機進程
- jmap:用于生成堆轉存快照dump文件。還可以查看堆內存使用情況和堆所使用的gc收集器??捎糜诮鉀Q堆的oom問題
jmap -histo [進程id]
- jstack:生成線程快照,可以定位死鎖問題。
jstack -l [進程id]
- jstat:統(tǒng)計信息監(jiān)視工具。顯示當前進程的堆內存使用情況、gc次數(shù)。
jstat -gcutil [進程id]
E代表eden區(qū)、S0、S1.
O代表老年代
P代表永久代
YGC代表年輕代MirrorGC
YGCT代表MirrorGc耗時
FGC代表FullGc
FGCT代表FullGc耗時
GCT代表總耗時
- jhat:用于查看jmap導出的dump文件,但是一般不用