一、JVM類加載機制
1、類加載過程
當我們用java命令加載某個類的main函數啟動程序時,首先需要通過類加載器把主類加載到JVM。
具體步驟:1、調用底層的jvm.dll創建java虛擬機(C++)
2、創建一個引導類加載器(C++)
3、C++調用java代碼創建JVM啟動器實例com.misc.Lanucher(由引導類加載器加載)
4、獲取自己的類加載器並加載
5、加載完成時會執行主類的main方法入口
6、程序運行結束時銷毀JVM
2、類加載過程的具體步驟
- 加載:在硬盤上查找並通過IO讀入字節碼文件,使用到類時才會加載,例如調用類的main方法,new對象等,在加載階段會在內存中生成一個java.lang.Class對象,作為方法區這個類的各種數據的訪問入口
- 驗證:校驗字節碼文件的正確性
- 准備:給類的靜態變量分配內存並賦予默認值
- 解析:將符號引用替換為直接引用,該階段會把一些靜態方法(符號引用,比如main()方法)替換為指向數據內存的指針或句柄(指針的指針),這個過程稱之為靜態鏈接過程,動態鏈接是在程序運行期間將符號引用替換為直接引用
- 初始化:對類的靜態變量初始化值,執行靜態代碼塊
注意:主類在運行過程中如果引用到其他類會逐步加載,jar、war包里的類不是一次性全部加載的,是使用到時才會加載。
3、JAVA類加載器
- 引導類加載器:負責加載lib目錄下的核心類庫,如rt.jar、charset.jar
- 擴展類加載器:負責加載lib目錄下ext擴展目錄中的jar包
- 應用程序類加載器:classPath路徑下的,就是加載你自己寫的那些類
- 自定義加載器:負責加載自定義路徑下的類
4、雙親委派機制
HOW?
- 首先,檢查指定類是否已經加載過,如果已經加載過了,就不需要再加載,直接返回。
- 如果沒有加載過,判斷一下是否有父加載器,如果有,則由父加載器加載
- 如果父加載器都沒有找到,則由當前加載器負責加載
WHY?
- 沙箱安全機制:防止核心類庫被隨意篡改
- 避免類的重復加載:當父加載器已經加載類該類時,子ClassLoader沒必要再重新加載
全盤委托機制
“全盤負責”當一個classLoader加載一個類時,除非顯示的使用另外一個加載器,否則該類所依賴的引用類也由這個ClassLoader載入。
自定義類加載器
java.lang.ClassLoader類有兩個核心方法
loadCLass():實現了雙親委派機制
findClass():默認是空,自定義加載器主要是重寫此方法
5、Tomcat幾個主要的類加載器
- commonLoader:Tomcat最基本的類加載器,加載的class可以被web容器及各個app所訪問
- catalinaLoader:Tomcat私有類加載器,加載路徑中的class對於webApp不可見
- sharedLoader:各個app共享的類加載器,加載路徑中的class對於所有Webapp可見
- WebappClassLoader:webapp私有的類加載器,比如加載war包里的相關類
6、Tomcat打破雙親委派機制
- 一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一個第三方類庫的不同版本,因此要保證每個應用程序的類庫都是獨立的,是相互隔離的
- 部署在同一個web容器中的相同類庫的相同版本可以共享
- web容器也有自己的依賴類庫,不能與應用程序的類庫混淆
- web容器要支持jsp的熱加載,我們知道jsp也是翻譯成class文件后執行的,要支持jsp修改后不用重啟
二、JVM整體結構及內存模型
關於元空間JVM參數:
XX:MaxMetaspaceSize:設置原空間最大值,默認是-1,即不限制,或者說只受限於本地內存大小
XX:MetaspaceSize:元空間觸發FullGc的初始伐值,默認是21M,達到該值就會觸發full gc進行卸載,同時收集器會對該值進行調整,如果釋放了大量空間就降低該值,如果釋放了較少空間就在不超過XX:MaxMetaspaceSize的情況下適當提高該值
由於調整元空間大小需要Full Gc,這是非常昂貴的操作,如果在剛啟動時就發生了Full Gc ,通常是由於永久代或元空間的大小發生了調整,一般建議XX:MaxMetaspaceSize和XX:MetaspaceSize 設置成一樣的值,並設置的比初始值要大,一般8G物理機內存將這兩個值都設置為256M
Xss:設置的count值越小,說明一個線程里能分配的棧幀就越少,但是對JVM來說能開啟的線程數就越多
JVM調優:就是盡可能讓對象在新生代里完成分配和回收,盡量別讓太多對象進入老年代,避免頻繁對老年代進行回收,給系統充足的內存大小,避免新生代頻繁的進行垃圾回收
三、JVM對象創建及內存分配機制
1、對象的創建
- 類加載檢查
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位一個類的符號引用,並檢查這個符號引用代表的類是否已經被加載、解析和初始化過。如果沒有,那必須先執行類的加載過程。分配內存 - 分配內存
在類加載檢查通過后,接下來虛擬機將為新生對象分配內存,對象所需內存大小在類加載完成后便可完全確定,為對象分配空間的任務等同於把一塊確定大小的內存從Java堆中划分出來。
划分內存的方法:
指針碰撞:Java內存排列是絕對工整的,用過的內存放一邊,空閑的內存放另外一邊,中間放着一個指針作為分界點的指示器
空閑列表:內存是不工整的,虛擬機維護一個列表,記錄哪些內存是可用的
並發問題的解決辦法:
CAS:虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性來對分配內存空間的動作進行同步處理
TLAB(本地線程緩沖):把內存分配的動作划分在不同的空間進行,即每個線程在Java堆中預先分配一小塊內存通過XX:+/UseTLAB參數來設定虛擬機是否使用TLAB(JVM會默認開啟XX:+UseTLAB),XX:TLABSize 指定TLAB大小。 - 初始化
內存分配完成后,虛擬機將分配到的內存空間都初始化為零值,如果使用TLAB,這一過程也可提前至TLAB分配時進行,這一步驟保證了實例字段在JAVA代碼中可以不賦初始值就直接使用。設置對象頭初始化零值之后,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息,對象的哈希碼,對象的GC分代年齡等信息,這些信息存放在對象頭之中。 - 對象在內存中的布局
對象頭:對象自身的運行時數據,如哈希碼,GC分代年齡、線程持有的鎖、鎖狀態標志、偏向線程ID、偏向時間戳。另一部分是類型指針,指向類的元數據的指針,通過這個指針確定是哪一個類的實例。
實例數據:
對齊填充:
什么是java對象的指針壓縮?
啟用指針壓縮:XX:+UseCompressedOops(默認開啟),禁止指針壓縮:XX:UseCompressedOops
為什么要進行指針壓縮?
1.在64位平台的HotSpot中使用32位指針,內存使用會多出1.5倍左右,使用較大指針在主內存和緩存之間移動數據,占用較大寬帶,同時GC也會承受較大壓力2.為了減少64位平台下內存的消耗,啟用指針壓縮功能3.在jvm中,32位地址最大支持4G內存(2的32次方),可以通過對對象指針的壓縮編碼、解碼方式進行優化,使得jvm只用32位地址就可以支持更大的內存配置(小於等於32G)4.堆內存小於4G時,不需要啟用指針壓縮,jvm會直接去除高32位地址,即使用低虛擬地址空間5.堆內存大於32G時,壓縮指針會失效,會強制使用64位(即8字節)來對java對象尋址,這就會出現1的問題,所以堆內存不要大於32G為好
2、對象內存分配
- 內存分配流程
- 對象棧上分配(依賴於逃逸分析和標量替換)
JVM通過逃逸分析確定對象不會被外部訪問,如果不會逃逸可以將對象在棧上分配內存,這樣該對象鎖占用的內存空間就可以隨棧幀出棧而銷毀,減輕了垃圾回收的壓力。
逃逸分析:當一個對象在方法中被定義后,可能被外部方法所引用,JDK7以后會默認開啟逃逸分析
標量替換:通過逃逸分析后,確定不會被外部所引用,JVM不會創建該對象,而是將對象分解若干個被這個方法所使用的成員變量,這些代替的成員變量在棧幀或寄存器上分配空間
聚合量:不可被進一步分解的量稱之為聚合量,例如java對象 - 對象在Eden區分配
Minor GC/Young GC:新生代垃圾收集動作,minor GC 回收非常頻繁,速度也比較快
Full GC/Major GC:回收老年代,年輕代的垃圾,回收速度比Minor GC慢十倍以上
Eden與Survivor區默認8:1:1
注意:當Eden區被分配完了時,虛擬機將發起一次Minor GC,GC期間又發現Survivor區滿了,只好把新生代的對象提前移到老年代中去
- 大對象直接進入老年代(避免對大對象內存的復制操作而降低效率)
大對象就是需要大量連續內存空間的對象(比如:字符串,數組)。JVM參數-XX:PretenureSizeThreshold可以設置大對象的大小,超過這個大小會直接進入老年代 - 長期存活的對象將進入老年代
對象在 Survivor 中每熬過一次 MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲,CMS收集器默認6歲,不同的垃圾收集器會略微有點不同),就會被晉升到老年代中。對象晉升到老年代 的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。
年齡1+年齡2+年齡n的多個年齡對象總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的對象都放入老年代。 - 老年代空間分配擔保機制
年輕代每次minor gc之前,jvm都會計算下老年代的剩余可用空間,如果這個空間小於現有年輕代里所有對象大小之和(包括垃圾對象),就會看 -XX:-HandlePromotionFailure 參數是否設置(jdk1.8默認設置),就會看老年代可用大小是否大於之前每一次minor gc后進入老年代的對象平均大小,如果小於或者沒有設置,那么就會觸發一次Full GC對老年代和年輕代一起回收,如果還是沒有空間就會發生OOM,當然minor gc 后老年代還是沒有空間放minor gc 中存活的對象,也會觸發 Full GC ,也會發生OOM - 如何判斷對象已經死亡
引用計數法:給對象添加一個引用計數器,每當有引用到的地方,計數器+1,失去引用,計數器-1,當計數器為0時,對象已經死亡
可達性分析算法:GC Roots對象作為起點,開始向下搜索引用的對象,找到的對象都標記為非垃圾對象,其余對象標記為垃圾對象 - 幾種常見的引用類型:
強引用:普通的變量引用
public static User user = new User();
如何判斷一個類是無用的類?
- 該類所有的實例已經被回收,java堆中不存在類的任何實例
- 加載該類的classLoader已經被回收
- 該類對應的class對象沒有在任何地方被引用
四、垃圾收集算法
1、分代收集理論
java堆一般分為年輕代和老年代,這樣我們就可以根據各塊的特點選擇合適的垃圾收集算法
2、復制算法
為了解決效率問題,復制算法出現了,他可以將內存分為大小相同的兩塊,每次使用其中的一塊,當這一塊內存使用完以后,將還存活的對象復制到另一塊中去,然后把使用的空間一次清理掉,這樣每次回收都是堆一半的內存進行回收。
3、標記清除算法
標記存活的對象,統一回收未標記的對象(也可以反過來)
會產生兩個問題:1、標記的對象太多,效率不高
2、標記清除后會產生大量的不連續碎片
4、標記整理算法
第一步與標記清除算法一樣,后續讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內存。
五、垃圾收集器
1、Serial收集器(-XX:+UseSerialGC-XX:+UseSerialOldGC)
單線程,並且會暫停其他所有線程(STW),新生代采用復制算法,老年代采用標記整理算法。
2、Parallel Scavenge收集器(-XX:+UseParallelGC(年輕代),-XX:+UseParallelOldGC(老年代))
Serial收集器的多線程版本
3、ParNew收集器(-XX:+UseParNewGC)
ParNew收集器其實跟Parallel收集器很類似,區別主要在於它可以和CMS收集器配合使用。
4、CMS收集器(-XX:+UseConcMarkSweepGC(old)) ()
- 初始標記:STW,記錄gc roots 直接能引用的對象,速度很快
- 並發標記:是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程, 這個過程耗時較長但是不需要停頓用戶線程, 可以與垃圾收集線程一起並發運行。因為用戶程序繼續運行,可能會有導致已經標記過的對象狀態發生改變。
- 重新標記:重新標記階段就是為了修正並發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短。
- 並發清理:開啟用戶線程,同時GC線程開始對未標記的區域做清掃。這個階段如果有新增對象會被標記為黑色不做任何處理
-
並發重置:重置本次GC過程中的標記數據。
-
缺點:對CPU資源敏感(會和服務搶資源);無法處理浮動垃圾(在並發標記和並發清理階段又產生垃圾,這種浮動垃圾只能等到下一次gc再清理了);它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生,當然通過參數-XX:+UseCMSCompactAtFullCollection可以讓jvm在執行完標記清除后再做整理執行過程中的不確定性,會存在上一次垃圾回收還沒執行完,然后垃圾回收又被觸發的情況,特別是在並發標記和並發清理階段會出現,一邊回收,系統一邊運行,也許沒回收完就再次觸發full gc,也就是"concurrent mode failure",此時會進入stop the world,用serial old垃圾收集器來回收
- 核心參數
1. -XX:+UseConcMarkSweepGC:啟用cms2. -XX:ConcGCThreads:並發的GC線程數3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做壓縮整理(減少碎片)4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后壓縮一次,默認是0,代表每次FullGC后都會壓縮一次5. -XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發FullGC(默認是92,這是百分比)6. -XX:+UseCMSInitiatingOccupancyOnly:只使用設定的回收閾值(-XX:CMSInitiatingOccupancyFraction設定的值),如果不指定,JVM僅在第一次使用設定值,后續則會自動調整7. -XX:+CMSScavengeBeforeRemark:在CMS GC前啟動一次minor gc,目的在於減少老年代對年輕代的引用,降低CMS GC的標記階段時的開銷,一般CMS的GC耗時 80%都在標記階段8. -XX:+CMSParallellnitialMarkEnabled:表示在初始標記的時候多線程執行,縮短STW9. -XX:+CMSParallelRemarkEnabled:在重新標記的時候多線程執行,縮短STW;
只要年輕代參數設置合理,老年代CMS的參數設置基本都可以用默認值
5、G1收集器(-XX:+UseG1GC)

-
初始標記(initial mark,STW):暫停所有的其他線程,並記錄下gc roots直接能引用的對象,速度很快 ;
-
並發標記(Concurrent Marking):同CMS的並發標記
-
最終標記(Remark,STW):同CMS的重新標記
-
篩選回收(Cleanup,STW):篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間(可以用JVM參數 -XX:MaxGCPauseMillis指定)來制定回收計划
6、ZGC收集器(-XX:+UseZGC)
TB級別收集器,不分代
六、常用命令
1、jps:查看所有java進程
2、jmap -histo 15322 (查看內存信息:實例個數、內存大小、類名)
3、jmap -heap 15322 (查看堆信息)
4、jmap‐dump:format=b,file=eureka.hprof 14660(內存很大的時候,可能會導不出來)
5、Jstack 找出占用cpu最高的線程堆棧信息
- top -p 15322 顯示java進程的內存情況
- 按H獲取每個每個線程的內存情況
- 找到內存和cpu占用最高的線程tid,假設是18929,轉換16進制49f1
- 查看對應對應堆棧信息找出可能存在問題的代碼
6、Jinfo 查看正在運行java程序的擴展參數
- jinfo -flags 15322 查看jvm參數
- jinfo -sysprops 15322 查看java系統參數
7、Jstat 查看堆內存各部分的使用量以及加載類的數量
- jstat -gc 15322 評估內存使用及GC壓力情況
S0C:第一個幸存區的大小,單位KB
S1C:第二個幸存區的大小S0U:第 一 個幸存區的使用大小 S1U:第二個幸存區的使用大小EC:伊甸園區的大小EU:伊甸園區的使用大小OC:老年代大小OU:老年代使用大小MC:方法區大小(元空間)MU:方法區使用大小CCSC:壓縮類空間大小CCSU:壓縮類空間使用大小YGC:年輕代垃圾回收次數YGCT:年輕代垃圾回收消耗時間,單位sFGC:老年代垃圾回收次數FGCT:老年代垃圾回收消耗時間,單位sGCT:垃圾回收消耗總時間,單位s - jstat -gccapacity 15322 (堆內存統計)NGCMN:新生代最小容量NGCMX:新生代最大容量NGC:當前新生代容量S0C:第 一 個幸存區大小S1C:第二個幸存區的大小EC:伊甸園區的大小OGCMN:老年代最小容量OGCMX:老年代最大容量OGC:當前老年代大小OC:當前老年代大小MCMN:最小元數據容量MCMX:最大元數據容量MC:當前元數據空間大小CCSMN:最小壓縮類空間大小CCSMX:最大壓縮類空間大小CCSC:當前壓縮類空間大小YGC:年輕代gc次數FGC:老年代GC次數
- jstat -gcnew 15322 (新生代垃圾回收統計)
S0C:第 一 個幸存區的大小S1C:第二個幸存區的大小S0U:第 一 個幸存區的使用大小S1U:第二個幸存區的使用大小TT:對象在新生代存活的次數MTT:對象在新生代存活的最大次數DSS:期望的幸存區大小EC:伊甸園區的大小EU:伊甸園區的使用大小YGC:年輕代垃圾回收次數YGCT:年輕代垃圾回收消耗時間 - jstat -gcnewcapacity 15322 (新生代內存統計)
NGCMN:新生代最小容量NGCMX:新生代最大容量NGC:當前新生代容量S0CMX:最大幸存1區大小S0C:當前幸存1區大小S1CMX:最大幸存2區大小S1C:當前幸存2區大小ECMX:最大伊甸園區大小EC:當前伊甸園區大小YGC:年輕代垃圾回收次數FGC:老年代回收次數 - jstat -gcold 15322 (老年代垃圾回收統計)
MC:方法區大小MU:方法區使用大小CCSC:壓縮類空間大小CCSU:壓縮類空間使用大小OC:老年代大小OU:老年代使用大小YGC:年輕代垃圾回收次數FGC:老年代垃圾回收次數FGCT:老年代垃圾回收消耗時間GCT:垃圾回收消耗總時間 - jstat -gcoldcapacity 15322 (老年代內存統計)
OGCMN:老年代最小容量
OGCMX:老年代最大容量OGC:當前老年代大小OC:老年代大小YGC:年輕代垃圾回收次數FGC:老年代垃圾回收次數FGCT:老年代垃圾回收消耗時間GCT:垃圾回收消耗總時間 - jstat -gcmetacapacity 15322 (元空間垃圾回收統計)
MCMN:最小元數據容量
MCMX:最大元數據容量MC:當前元數據空間大小CCSMN:最小壓縮類空間大小CCSMX:最大壓縮類空間大小CCSC:當前壓縮類空間大小YGC:年輕代垃圾回收次數FGC:老年代垃圾回收次數FGCT:老年代垃圾回收消耗時間GCT:垃圾回收消耗總時間 - jstat -gcutil 15322S0:幸存1區當前使用比例S1:幸存2區當前使用比例E:伊甸園區使用比例O:老年代使用比例M:元數據區使用比例CCS:壓縮使用比例YGC:年輕代垃圾回收次數FGC:老年代垃圾回收次數FGCT:老年代垃圾回收消耗時間GCT:垃圾回收消耗總時間
七、如何調優?
full gc比minor gc還多的原因有哪些?
1、元空間不足導致頻繁full gc
2、顯示調用System.gc()造成多余full gc ,-XX:+DisableExplicitGC 參數金庸
3、老年代空間分配擔保機制
什么是內存泄漏?
常見的多級緩存架構redis+jvm緩存,很多程序員圖方便jvm緩存就只是適用一個hashmap,結果這個緩存map越來越大,一直占用老年代的空間,時間長了就會發生full gc,對於一些老舊數據沒有及時清理,時間長了除了會導致full gc 還會導致OOM。這種情況考慮使用一些成熟的JVM框架來解決,如ehcache等自帶的LRU數據淘汰算法的框架作為JVM級的緩存
三種字符串操作
String s = “abc”;// s指向常量池中的引用(用equals方法檢查常量池中有沒有這個常量,直接有返回引用,沒有創建一個返回對象引用)
String s1 = new(“abc”);// s1指向內存中的對象引用(equals方法檢查常量池中有沒有這個常量,沒有創建,然后在堆中在創建一個對象,返回引用)
String s2 = s1.intern(); // 如果池中已經包含一個等於s1對象的字符串,則返回池子中的字符串,否則直接指向s1
1 String s0="zhuge"; 2 String s1="zhuge"; 3 String s2="zhu" + "ge"; 4 System.out.println( s0==s1 ); //true 5 System.out.println( s0==s2 ); //true
都是字符串常量,在編譯時期就確定了
1 String s0="zhuge"; 2 String s1=new String("zhuge"); 3 String s2="zhu" + new String("ge"); 4 System.out.println( s0==s1 ); // false 5 System.out.println( s0==s2 ); // false 6 System.out.println( s1==s2 ); // false new()創建的字符串不是常量,在編譯時期不能確定
1 String a = "a1"; 2 String b = "a" + 1; 3 System.out.println(a == b); // true 4 String a = "atrue"; 5 String b = "a" + "true"; 6 System.out.println(a == b); // true 7 String a = "a3.4"; 8 String b = "a" + 3.4; 10 System.out.println(a == b); // true
1、true、3.4在字符串之后在編譯時期就確定為常量
String a = "ab"; String bb = "b"; String b = "a" + bb; System.out.println(a == b); // false bb作為變量在編譯時期不確定,在運行時才確定,會生成一個新的對象
String a = "ab"; final String bb = "b"; String b = "a" + bb; System.out.println(a == b); // true final在編譯時期被解析為常量
String a = "ab"; final String bb = getBB(); String b = "a" + bb; System.out.println(a == b); // false private static String getBB() { return "b"; } getBB()在編譯時期無法確定
String s = "a" + "b" + "c"; //就等價於String s = "abc"; String a = "a"; String b = "b"; String c = "c"; String s1 = a + b + c; // 編譯時期a\b\c作為變量不確定
八種基本類型的包裝類和對象池