更新時間:2022-12-27 12:32:58 來源:動力節(jié)點 瀏覽1882次
Stop-the-World:
JVM由于要執(zhí)行GC而停止了應(yīng)用程序的執(zhí)行稱之為Stop-the-World,該情形會在任何一種GC算法中發(fā)生。當(dāng)Stop-the-world發(fā)生時,除了GC所需的線程以外,所有線程都處于等待狀態(tài)直到GC任務(wù)完成。事實上,GC優(yōu)化很多時候就是指減少Stop-the-world發(fā)生的時間,從而使系統(tǒng)具有高吞吐 、低停頓 的特點。
(1)程序計數(shù)器
記錄當(dāng)前線程所執(zhí)行的字節(jié)碼行號,用于獲取下一條執(zhí)行的字節(jié)碼。
當(dāng)多線程運行時,每個線程切換后需要知道上一次所運行的狀態(tài)、位置。
由此也可以看出程序計數(shù)器是每個線程私有的。
(2)虛擬機棧
虛擬機棧由一個一個的棧幀組成,棧幀是在每一個方法調(diào)用時產(chǎn)生的。
每一個棧幀由局部變量區(qū)、操作數(shù)棧等組成。每創(chuàng)建一個棧幀壓棧,當(dāng)一個方法執(zhí)行完畢之后則出棧。
如果出現(xiàn)方法遞歸調(diào)用出現(xiàn)死循環(huán)的話就會造成棧幀過多,最終會拋出 StackOverflowError。
若線程執(zhí)行過程中棧幀大小超出虛擬機棧限制,則會拋出 StackOverflowError。
若虛擬機棧允許動態(tài)擴展,但在嘗試擴展時內(nèi)存不足,或者在為一個新線程初始化新的虛擬機棧時申請不到足夠的內(nèi)存,則會拋出 OutOfMemoryError。
這塊內(nèi)存區(qū)域也是線程私有的。
(3)Java堆
Java 堆是整個虛擬機所管理的最大內(nèi)存區(qū)域,所有的對象創(chuàng)建都是在這個區(qū)域進(jìn)行內(nèi)存分配。
可利用參數(shù) -Xms -Xmx 進(jìn)行堆內(nèi)存控制。
這塊區(qū)域也是垃圾回收器重點管理的區(qū)域,由于大多數(shù)垃圾回收器都采用分代回收算法,所有堆內(nèi)存也分為 新生代、老年代,可以方便垃圾的準(zhǔn)確回收。
這塊內(nèi)存屬于線程共享區(qū)域。
(4)方法區(qū)
方法區(qū)主要用于存放已經(jīng)被虛擬機加載的類信息,如常量,靜態(tài)變量。 這塊區(qū)域也被稱為永久代。
可利用參數(shù) -XX:PermSize -XX:MaxPermSize 控制初始化方法區(qū)和最大方法區(qū)大小。
(5)元數(shù)據(jù)區(qū)
在 JDK1.8 中已經(jīng)移除了方法區(qū)(永久代),并使用了一個元數(shù)據(jù)區(qū)域進(jìn)行代替(Metaspace)。
默認(rèn)情況下元數(shù)據(jù)區(qū)域會根據(jù)使用情況動態(tài)調(diào)整,避免了在 1.7 中由于加載類過多從而出現(xiàn) java.lang.OutOfMemoryError: PermGen。
但也不能無線擴展,因此可以使用 -XX:MaxMetaspaceSize來控制最大內(nèi)存。
(6)運行時常量池
運行時常量池是方法區(qū)的一部分,其中存放了一些符號引用。當(dāng) new 一個對象時,會檢查這個區(qū)域是否有這個符號的引用。
(7)直接內(nèi)存
直接內(nèi)存又稱為 Direct Memory(堆外內(nèi)存),它并不是由 JVM 虛擬機所管理的一塊內(nèi)存區(qū)域。
有使用過 Netty 的朋友應(yīng)該對這塊并內(nèi)存不陌生,在 Netty 中所有的 IO(nio) 操作都會通過 Native 函數(shù)直接分配堆外內(nèi)存。
它是通過在堆內(nèi)存中的 DirectByteBuffer 對象操作的堆外內(nèi)存,避免了堆內(nèi)存和堆外內(nèi)存來回復(fù)制交換復(fù)制,這樣的高效操作也稱為零拷貝。
既然是內(nèi)存,那也得是可以被回收的。但由于堆外內(nèi)存不直接受 JVM 管理,所以常規(guī) GC 操作并不能回收堆外內(nèi)存。它是借助于老年代產(chǎn)生的 fullGC 順便進(jìn)行回收。同時也可以顯式調(diào)用 System.gc() 方法進(jìn)行回收(前提是沒有使用 -XX:+DisableExplicitGC 參數(shù)來禁止該方法)。
值得注意的是:由于堆外內(nèi)存也是內(nèi)存,是由操作系統(tǒng)管理。如果應(yīng)用有使用堆外內(nèi)存則需要平衡虛擬機的堆內(nèi)存和堆外內(nèi)存的使用占比。避免出現(xiàn)堆外內(nèi)存溢出。
(8)常用參數(shù)
通過上圖可以直觀的查看各個區(qū)域的參數(shù)設(shè)置。
常見的如下:
-Xms64m 最小堆內(nèi)存 64m.
-Xmx128m 最大堆內(nèi)存 128m.
-XX:NewSize=30m 新生代初始化大小為30m.
-XX:MaxNewSize=40m 新生代最大大小為40m.
-Xss=256k 線程棧大小。
-XX:+PrintHeapAtGC 當(dāng)發(fā)生 GC 時打印內(nèi)存布局。
-XX:+HeapDumpOnOutOfMemoryError 發(fā)送內(nèi)存溢出時 dump 內(nèi)存。
新生代和老年代的默認(rèn)比例為 1:2,也就是說新生代占用 1/3的堆內(nèi)存,而老年代占用 2/3 的堆內(nèi)存。
可以通過參數(shù) -XX:NewRatio=2 來設(shè)置老年代/新生代的比例。
下圖便是 Java 對象的創(chuàng)建過程,我建議最好是能默寫出來,并且要掌握每一步在做什么。
Step1:類加載檢查
虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。
Step2:分配內(nèi)存
在類加載檢查通過后,接下來虛擬機將為新生對象分配內(nèi)存。對象所需的內(nèi)存大小在類加載完成后便可確定,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從 Java 堆中劃分出來。分配方式有 “指針碰撞” 和 “空閑列表” 兩種,選擇那種分配方式由 Java 堆是否規(guī)整決定,而 Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
內(nèi)存分配的兩種方式:(補充內(nèi)容,需要掌握)
選擇以上兩種方式中的哪一種,取決于 Java 堆內(nèi)存是否規(guī)整。而 Java 堆內(nèi)存是否規(guī)整,取決于 GC 收集器的算法是"標(biāo)記-清除",還是"標(biāo)記-整理"(也稱作"標(biāo)記-壓縮"),值得注意的是,復(fù)制算法內(nèi)存也是規(guī)整的
內(nèi)存分配并發(fā)問題(補充內(nèi)容,需要掌握)
在創(chuàng)建對象的時候有一個很重要的問題,就是線程安全,因為在實際開發(fā)過程中,創(chuàng)建對象是很頻繁的事情,作為虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機采用兩種方式來保證線程安全:
CAS+失敗重試: CAS 是樂觀鎖的一種實現(xiàn)方式。所謂樂觀鎖就是,每次不加鎖而是假設(shè)沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性。
TLAB: 為每一個線程預(yù)先在 Eden 區(qū)分配一塊兒內(nèi)存,JVM 在給線程中的對象分配內(nèi)存時,首先在 TLAB 分配,當(dāng)對象大于 TLAB 中的剩余內(nèi)存或 TLAB 的內(nèi)存已用盡時,再采用上述的 CAS 進(jìn)行內(nèi)存分配
Step3:初始化零值
內(nèi)存分配完成后,虛擬機需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值。
Step4:設(shè)置對象頭
初始化零值完成之后,虛擬機要對對象進(jìn)行必要的設(shè)置,例如這個對象是那個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據(jù)虛擬機當(dāng)前運行狀態(tài)的不同,如是否啟用偏向鎖等,對象頭會有不同的設(shè)置方式。
以上就是關(guān)于“Java數(shù)據(jù)同步解決方案”的介紹,大家如果想了解更多相關(guān)知識,可以關(guān)注一下本站的Java在線學(xué)習(xí),里面的課程內(nèi)容細(xì)致全面,很適合沒有基礎(chǔ)的小伙伴學(xué)習(xí),希望對大家能夠有所幫助。
相關(guān)閱讀
初級 202925
初級 203221
初級 202629
初級 203743