內(nèi)存結(jié)構(gòu):
方法區(qū):
用于儲存已被虛擬機加載的類信息,常量,靜態(tài)變量,即時編譯器編譯后的代碼等數(shù)據(jù),是線程共享的
異常:當方法區(qū)無法滿足內(nèi)存分配需求(-XX:MaxPermSize)時,將會拋出OutOfMemoryError:PermGen space;異常.
JDK1.8 移除了方法區(qū),增加元空間,元空間內(nèi)存只受本地內(nèi)存限制,元空間可以在運行時動態(tài)調(diào)整,可以使用-XX:MaxMatespaceSize 設置本地內(nèi)存分配給元空間的最大內(nèi)存,元空間與堆不相連,但是與堆共享物理內(nèi)存
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-UseCompressedClassPointers -XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m
第一個參數(shù)用于打印GC日志耕赘;
第二個參數(shù)用于打印對應的時間戳;
第三個參數(shù)-XX:-UseCompressedClassPointer表示在Metaspace中不要開辟出一塊新的空間(Compressed Class Space)狼忱,如果開辟這塊空間的話,該空間默認大小是1G一睁,所以我們關閉該功能钻弄,此時再設置Metaspace的大小者吁;
堆內(nèi)存:
堆是JVM管理的最大的一塊內(nèi)存,是線程共享的,用于存放對象實例以及數(shù)據(jù).堆也是垃圾回收器主要管理的區(qū)域,也就GC堆.
虛擬機啟動時就會創(chuàng)建堆,可通過-Xmx窘俺、-Xms調(diào)節(jié)堆大小
異常:如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法進行再擴展(-Xmx,-Xms)了,將會拋出java.lang.OutOfMemoryError: Java heap space異常
1.指針碰撞法
假設Java堆中內(nèi)存時完整的,已分配的內(nèi)存和空閑內(nèi)存分別在不同的一側(cè)复凳,通過一個指針作為分界點瘤泪,需要分配內(nèi)存時,僅僅需要把指針往空閑的一端移動與對象大小相等的距離育八。使用的GC收集器:Serial对途、ParNew,適用堆內(nèi)存規(guī)整(即沒有內(nèi)存碎片)的情況下单鹿。
2.空閑列表法
事實上掀宋,Java堆的內(nèi)存并不是完整的深纲,已分配的內(nèi)存和空閑內(nèi)存相互交錯仲锄,JVM通過維護一個列表,記錄可用的內(nèi)存塊信息湃鹊,當分配操作發(fā)生時儒喊,從列表中找到一個足夠大的內(nèi)存塊分配給對象實例,并更新列表上的記錄币呵。使用的GC收集器:CMS怀愧,適用堆內(nèi)存不規(guī)整的情況下。
內(nèi)存分配并發(fā)問題
在創(chuàng)建對象的時候有一個很重要的問題余赢,就是線程安全芯义,因為在實際開發(fā)過程中,創(chuàng)建對象是很頻繁的事情妻柒,作為虛擬機來說扛拨,必須要保證線程是安全的,通常來講举塔,虛擬機采用兩種方式來保證線程安全:
CAS: CAS 是樂觀鎖的一種實現(xiàn)方式绑警。所謂樂觀鎖就是求泰,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試计盒,直到成功為止渴频。虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性。
TLAB: 為每一個線程預先分配一塊內(nèi)存北启,JVM在給線程中的對象分配內(nèi)存時卜朗,首先在TLAB分配,當對象大于TLAB中的剩余內(nèi)存或TLAB的內(nèi)存已用盡時咕村,再采用上述的CAS進行內(nèi)存分配聊替。
內(nèi)存空間分配完成后會初始化為 0(不包括對象頭),接下來就是填充對象頭培廓,把對象是哪個類的實例惹悄、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼肩钠、對象的 GC 分代年齡等信息存入對象頭泣港。
執(zhí)行 new 指令后執(zhí)行 init 方法后才算一份真正可用的對象創(chuàng)建完成;
對象的內(nèi)存布局
在 HotSpot 虛擬機中价匠,分為 3 塊區(qū)域:對象頭(Header)当纱、實例數(shù)據(jù)(Instance Data) 和 對齊填充(Padding)
對象頭(Header):包含兩部分,第一部分用于存儲對象自身的運行時數(shù)據(jù)踩窖,如哈希碼坡氯、GC分代年齡、鎖狀態(tài)標志洋腮、線程持有的鎖箫柳、偏向線程 ID、偏向時間戳等啥供,32 位虛擬機占 32 bit悯恍,64 位虛擬機占 64 bit。官方稱為 ‘Mark Word’伙狐;
第二部分是類型指針涮毫,即對象指向它的類的元數(shù)據(jù)指針,虛擬機通過這個指針確定這個對象是哪個類的實例贷屎,另外罢防,如果是 Java 數(shù)組,對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)唉侄,因為普通對象可以通過 Java 對象元數(shù)據(jù)確定大小咒吐,而數(shù)組對象不可以
實例數(shù)據(jù)(Instance Data):程序代碼中所定義的各種成員變量類型的字段內(nèi)容(包含父類繼承下來的和子類中定義的);
對齊填充(Padding):不是必然需要,主要是占位渤滞,保證對象大小是某個字節(jié)的整數(shù)倍贬墩;
對象的訪問
使用對象時,我們是通過棧上的 reference 引用來操作堆上的具體對象妄呕;
Sun Hotspot虛擬機使用直接指針訪問具體對象陶舞;
java 虛擬機棧:
是java方法執(zhí)行的內(nèi)存模型,每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀,用于存儲局部變量表,操作數(shù)棧,動態(tài)鏈接,方法出口燈信息.每一個方法從調(diào)用直至執(zhí)行完成的過程,就是對應著這個棧幀在虛擬機棧中的入棧和出棧的過程,該區(qū)域是線程私有的,她的生命周期 跟線程的生命周期相同.
局部變量表:包含方法中的參數(shù)和方法內(nèi)定義的局部變量,如果的局部變量是一個對象的引用, 那么這個引用指向堆中的對象
操作數(shù)棧:也叫做操作站,他是一個先進后出的棧,當一個方法剛剛開始執(zhí)行時,其操作數(shù)棧是空的,隨著方法執(zhí)行,會從局部變量表或者實例對象的字段中復制常量或者變量寫入操作數(shù)棧,再隨著計算的進行將棧中元素出棧道局部變量表或返回方法調(diào)用者,也就是入展和出棧操作,一個完整的方法執(zhí)行期間往往包含多個這樣的入展/出棧的過程
動態(tài)鏈接:一個方法要調(diào)用其他方法,需要將這些方法的符號引用轉(zhuǎn)化為其在內(nèi)存地址中的直接引用,而符號引用存在于方法區(qū)中的運行時常量池,所以需要在運行時動態(tài)將這些符號引用轉(zhuǎn)化為直接引用.
返回地址:方法不管是正常執(zhí)行結(jié)束還是異常退出,需要放回方法被調(diào)用的位置.
本地方法棧:與虛擬機棧類似,只不過本地方法棧是為虛擬機使用到的native方法服務的
程序計數(shù)器:是一塊較小的內(nèi)存空間,它可以看做當前線程所執(zhí)行的字節(jié)碼的行號指示器.字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支,循環(huán),跳轉(zhuǎn),異常處理,線程恢復等基礎功能都需要依賴于這個計數(shù)器.
程序計數(shù)器是線程私有的,每個線程依賴于每個線程中的程序計數(shù)器來確保線程切換后能恢復到正確的執(zhí)行位置.
如果,在執(zhí)行的是Java方法,則這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址.如果是本地native方法,則該計數(shù)器記錄的為空(Undefined).
垃圾回收
垃圾回收機制:
不定時的去堆內(nèi)存中清理不可達對象.垃圾回收器執(zhí)行是自動的,程序員只能通過System.gc去建議垃圾回收器進行垃圾回收,但是是否執(zhí)行,什么時候執(zhí)行都是不可控的.
finalize方法:Java中使用finalize()方法在垃圾回收器將對象從內(nèi)存中清除出去前,做必要的清理工作.這個方法是由垃圾收集器在確定這個對象沒有被引用時對這個對象調(diào)用的。它是在Object類中定義的绪励,因此所有的類都繼承了它肿孵。子類覆蓋finalize()方法以整理系統(tǒng)資源或者執(zhí)行其他清理工作。finalize()方法是在垃圾收集器刪除對象之前對這個對象調(diào)用的疏魏。
GC線程是守護線程.
Java堆內(nèi)存可化為分新生代,老年代. 新生代,老年代內(nèi)存比例默認1:2(該值可以通過參數(shù) -XX:NewRatio來指定),而新生代可細分為Eden區(qū),from Survivor, to Survivor區(qū),比例8:1:1(可通過-XX:SurvivorRatio來指定)
新生代(Young):剛出生不久的對象,存放在新生代里,存放不是經(jīng)常使用的對象.
老年代(Old):存放比較活躍的對象,或者說是存在較久的年老的對象.
判斷對象是否已死:
1.引用計數(shù)法:給對象添加一個引用計數(shù)器,每當有一個地方引用它時,計數(shù)器值就加1;當引用失效時,計數(shù)器值就減1;任何時刻計數(shù)器為0的對象就是不可能再被使用的.
引用計數(shù)法弊端:很難解決對象之間的互相循環(huán)引用的問題,導致,本已經(jīng)沒有再引用的對象,因為相互引用著而導致計數(shù)器不為0,而無法回收,所以,現(xiàn)在Java虛擬機不采用引用計數(shù)法.
2.根搜索算法(可達性分析算法):通過一系列的稱為"GC Roots"的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑成為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的.如下圖所示:
可作為GC Roots對象包括下面幾種:
1)虛擬機棧(棧幀中的本地變量表)中引用的對象.
2)方法區(qū)中類靜態(tài)屬性引用的對象
3)方法區(qū)中常量引用的對象
4)本地方法棧中JNI(native 方法)引用的對象.
不可達對象被回收的過程(兩次標記):
當一個對象被垃圾收集器認為不可達時,此時這個對象會被第一次標記,并進行一次篩選,篩選的條件就是該對象有沒有必要執(zhí)行finalize()方法.當對象沒有覆蓋finalize()方法,或著已經(jīng)被虛擬機執(zhí)行過,則認為沒有必要執(zhí)行(一個對象的finalize方法只會被調(diào)用一次).
虛擬機會將這些有必要執(zhí)行的對象放置到一個叫做F-Queue的隊列中,并在稍后由虛擬機自動建立的,低優(yōu)先級的Finalizer線程中取執(zhí)行它的finalize()方法,但是虛擬機只去觸發(fā)調(diào)用,不保證會等待finalize()執(zhí)行結(jié)束.
執(zhí)行finalize()是對象逃離死亡的最后的機會,只要在finalize()方法中重新與引用鏈連接上即可.重新建立上連接的對象,會在回收器在隊列里做第二次標記時,將之移除隊列,使其逃出升天.而,沒有重新建立上連接的對象在第二次被標記后,就真的被回收了.
垃圾收集算法:
1.標記-清除算法:
算法分為"標記"和"清除"兩個階段,首先標記處所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象.
缺陷:1.效率問題,標記 和 清除 兩個過程效率都不高.
2.空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導致以后再程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作.
2.復制算法:
復制算法將內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊,當這一塊用完了,就將還存活著的對象復制到另外一塊內(nèi)存上面,然后再把已使用過的內(nèi)存空間一次清除掉.
因為IBM公司的研究表明,98%的對象都是"朝生夕死",所以,一般不會按1:1去劃分空間,而是分為一個Eden區(qū)和兩個survivor區(qū),每次都使用一個Eden區(qū)和一個survivor區(qū)的空間.當執(zhí)行回收時,將Eden和from survivor區(qū)的還存活的對象一次性的復制到to survivor區(qū)中,然后清理Eden和from survivor區(qū).HotSpot 默認Eden:survivor=8:1.
但是,當survivor區(qū)內(nèi)存不夠用時,需要依賴老年代進行分配擔保(Handle Promotion),大概意思是,在垃圾回收時,如果to survivor的空間不足以存放Eden和 from survivor空間存活的對象時,這些對象將直接通過分配器擔保機制進入老年代(一般對象在from , to之間來回15此才會被移進老年代,15次這個是由JVM參數(shù)MaxTenuringThreshold決定的,默認是15).
優(yōu)點:在存活對象不多的時候,效率比較高,并解決碎片化問題.
缺點:會造成一部分內(nèi)存的浪費,并且如果存活的對象比較大,復制的效率會比較低.
3.標記-整理算法(標記-壓縮算法):
標記-整理算法 與標記-清除算法基本一樣,只是后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后再直接清理掉端邊意外的內(nèi)存.
優(yōu)點:解決了內(nèi)存碎片化的問題.
缺點:壓縮階段,由于移動了可用對象,則需要更新對象的引用.
4.分代收集算法
分代收集算法將堆空間分為新生代,老年代.根據(jù)各個年代的特點采用最適用的手機算法.新生代中,每次垃圾收集(young GC)時都會有大批對象死去,只有少量存活,一般采用復制算法.老年代(Full GC)中因為對象對象存活率高,沒有額外的空間進行分配擔保,就必須使用"標記-清除"或者"標記-整理"算法.
young GC(Minor GC):
當Eden空間滿的時候會觸發(fā),survivor滿的時候不會觸發(fā).一般young GC 較為頻繁,回收速度也比較快.
Full GC(Major GC):
當老年代滿了的時候會引發(fā)Full GC,Full GC 將會同時回收年輕代和老年代,即一般Full GC 會伴隨至少一次的young GC.當永久代(方法區(qū))滿的時候也會引發(fā)Full GC,會導致Class,Method 元信息的卸載(Java 8中,移除了永久代,新加了一個元數(shù)據(jù)區(qū)的native內(nèi)存區(qū)).Full GC速度比較慢,一般會比young GC慢10倍以上.
JVM 參數(shù)配置:
-Xmx20M -Xms20M -Xmn1M -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
1>在實際工作中停做,我們可以直接將初始的堆大小與最大堆大小相等,這樣的好處是可以減少程序運行時垃圾回收次數(shù),從而提高效率
Java 堆溢出:
Java堆用于存儲對象實例,只要不斷創(chuàng)建對象,并且保證GC Roots到對象之間有可達路徑來避免垃圾收集器清除這些對象,那么在對象數(shù)量到達最大堆的容量限制后就會產(chǎn)生內(nèi)存溢出(Java.lang.OutOfMemoryError: Java heap space).
要解決這個問題,一般需要通過內(nèi)存映像分析工具(idea JVM Debugger Memory View)對Dump出來的堆轉(zhuǎn)存儲快照進行分析,確認內(nèi)存中的對象是否是必要的.
對象是必要的則是內(nèi)存溢出,對象不必要的話就是內(nèi)存泄露.
如果是內(nèi)存泄露,進一步查看泄露對象到GC Roots的引用鏈,找到泄露對象為什么與GC Roots相關聯(lián),定位問題,解決問題.
如果是內(nèi)存溢出,查看代碼是否存在某些對象生命周期過長,持有狀態(tài)時間過長的情況,嘗試減少程序運行期間的內(nèi)存消耗.另外,確認-Xms,-Xmx參數(shù),與物理內(nèi)存相比較,確認是否可以對堆內(nèi)存進行增加.
垃圾收集器:
1>serial收集器:
serial收集器是最基本發(fā)展歷史最久的收集器,也是JDK1.3之前,唯一的收集器.serial收集器是一個單線程的收集器,它只會使用一個CPU或一條收集線程去完成垃圾收集工作,并且更重要的是它在進行垃圾收集時,必須暫停其他所有的線程工作,直到它收集結(jié)束.
它是虛擬機運行在Client模式下的默認新生代收集器.
優(yōu)點:與其他收集器相比,serial收集器簡單高效,對于限定單個CPU環(huán)境,serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率.
2>ParNew 收集器
ParNew收集器其實就是Serial收集器的多線程版本,除了在收集垃圾時,采用多線程之外,與Serial基本一樣.
但是ParNew是運行在server模式下的虛擬機中首選的新生代收集器.因為在單CPU下效率隨沒有serial高,但是在多CPU環(huán)境下效率顯著,一般它默然開啟的線程數(shù)與CPU數(shù)量相同.另外一個原因就是,目前只有它可以與CMS(老年代)收集器配合工作.
3>Parallel Scavenge收集器
Parallel Scavenge收集器與ParNew收集器類似,不同的是Parallel Scavenge收集器提供了兩個參數(shù)
-XX:MaxGCPauseMillis :該參數(shù)允許設定一個大于0的毫秒數(shù),表示收集器盡量在垃圾收集時不超過這個時間,但是這個是以犧牲吞吐量和新生代空間內(nèi)存為代價的.設置的時間越短,那收集的就越頻繁.
-XX:GCTimeRatio :參數(shù)允許設置一個大于0且小于100的整數(shù).也就是垃圾收集時間占總時間的比率,默認值是99,即允許最大的垃圾回收時間占比為1/100%
由于它與吞吐量關系密切,所以也稱作 吞吐量優(yōu)先收集器.
4>CMS收集器 -XX:+UseConcMarkSweepGC
CMS收集器是一種以獲取最短回收停頓時間為目標的收集器.應用于互聯(lián)網(wǎng)或者B/S系統(tǒng)的服務端上,采用 標記-清除 算法,收集老年代空間的垃圾.
CMS收集器收集分為四個步驟:
1>初始標記,標記GC Roots能夠直接關聯(lián)到的對象,速度很快.
2>并發(fā)標記,根據(jù)GC Roots 進行tracing追蹤
3>重新標記,修正在并發(fā)標記過程中,因用戶程序繼續(xù)運作而導致的標記變動的那一部分對象的標記記錄.
4>并發(fā)清除,將所有標記的對象清除.
CMS收集器的四個過程中,只有初始標記和重新標記的時候需要 stop the world,而耗時最久的并發(fā)標記和并發(fā)清除,均是并發(fā)進行,所以效率極高.
缺點:
1>CMS收集器對CPU資源非常敏感.因為面向并發(fā)設計,所以對CPU資源比較敏感,它雖不會讓用戶應用程序停止,但是它會占用一部分線程資源,而導致用戶應用程序變慢.默認收集線程為(CPU數(shù)+3)/4.
2>CMS收集器無法收集浮動資源(由于CMS并發(fā)清理過程用戶線程還在運行著,所以還有新的垃圾產(chǎn)生,這一部分垃圾出現(xiàn)在標記過程之后,CMS無法再檔次收集中處理他們,只好留待下一次,這樣的垃圾叫做浮動垃圾),可能導致出現(xiàn)Concurrent Mode Failure 失敗而導致一次serial Old 收集器的 Full GC.
因為在垃圾清除的過程中,用戶程序還在進行,那么就需要保留一定的內(nèi)存來供用戶使用,可以通過-XX:CMSInitiatingOccupancyFraction 來設置,JDK1.5中默認當老年代使用了68%時就會觸發(fā)CMS收集器回收垃圾.而JDK1.6中默認為92%.
如果在CMS運行期間,預留的內(nèi)存無法滿足用戶程序的需求時,就會出現(xiàn) Concurrent Mode Failure失敗,這時會啟動備案,臨時啟動Serial Old收集器重新進行老年代的垃圾收集.這樣停頓時間就會很長.
3>因為采用的是標記清除法,所以會有碎片化的問題
5>G1收集器
G1收集器是當今收集器最前沿的技術,G1是一款面向服務端應用的垃圾收集器.
G1對內(nèi)存的劃分與傳統(tǒng)的不同,G1把內(nèi)存劃分為多個大小相同的Region(默認512K),Region邏輯上連續(xù),物理內(nèi)存地址不連續(xù).同時每個Region被標記成E大莫、S蛉腌、O、H只厘,分別表示Eden烙丛、Survivor、Old羔味、Humongous河咽。其中E、S屬于年輕代赋元,O與H屬于老年代忘蟹。如下圖
H表示Humongous。從字面上就可以理解表示大的對象(下面簡稱H對象)搁凸。當分配的對象大于等于Region大小的一半的時候就會被認為是巨型對象媚值。H對象默認分配在老年代,可以防止GC的時候大對象的內(nèi)存拷貝
G1收集器過程如下:
1>初始標記:標記GC Roots能直接關聯(lián)到的對象,并修改TAMS(Next Top at Mark Start)的值,讓下一個階段用戶程序并發(fā)運行時,能在正確的可用的region中創(chuàng)建對象,這階段需要停頓線程,但耗時很短.
2>并發(fā)標記:是從GC Roots開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時時間較長,但可用與用戶程序并發(fā)進行.
3>最終標記:是為了修正在并發(fā)標記期間因用戶程序繼續(xù)進行而導致標記產(chǎn)生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs中,最終標記階段需要把 Remembered Set Logs中數(shù)據(jù)合并到 Remembered Set中,這段時間需要停頓線程,但是可并行進行.
4>篩選回收:最后在篩選回收階段首先對各個region的回收價值和成本進行排序,根據(jù)用戶所期望的GC停頓時間來制定回收計劃,這個階段是可以和用戶線程一起并發(fā)執(zhí)行的,但是因為一次只回收一部分region,時間是可以根據(jù)用戶控制的,而且停頓用戶線程將大幅度提高收集效率.
G1收集器特點:
1>G1可以回收年輕代和老年大的空間,年輕代采用復制算法,老年代采用標記整理算法,解決碎片化問題
2>可預測停頓,能夠讓用戶設定在M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過N毫秒.