JVM相關(guān)

java程序執(zhí)行過程

java執(zhí)行過程.jpg

運行時數(shù)據(jù)區(qū)域劃分

數(shù)據(jù)區(qū).jpg
線程私有區(qū)域
1. 程序計數(shù)器:

1.1. 這塊內(nèi)存區(qū)域很小,是當(dāng)前線程所執(zhí)行的字節(jié)碼行號的指示器。字節(jié)碼解釋器通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。

1.2. 線程執(zhí)行java方法,程序計數(shù)器才有值,保存當(dāng)前需要執(zhí)行的指令地址,如果執(zhí)行的是native方法,則這個計數(shù)器是未定義即是空的。

1.3. 在jvm中,多線程是通過線程輪流切換來獲得cpu執(zhí)行時間。在任何一具體時刻,一個cpu的內(nèi)核只會執(zhí)行一條線程中的指令。所以為了使得每個線程在線程切換后能夠恢復(fù)到切換前的狀態(tài),每個線程都要需要自己獨立且互不干擾的程序計數(shù)器。

2. java棧/虛擬機棧

2.1. 生命周期和線程生命周期相同,每個方法執(zhí)行都會創(chuàng)建一個棧幀。
2.2. 存儲局部變量表,操作數(shù)棧,指向當(dāng)前方法所屬的類的運行時常量池的引用,方法返回地址和一些額外的附加信息。每一個方法從調(diào)用到執(zhí)行完畢的過程,對應(yīng)一個棧幀在虛擬機中入棧到出棧的過程。(局部變量表就是存儲局部變量,包過在方法中聲明的非靜態(tài)變量以及函數(shù)形參。對于基礎(chǔ)類型直接存儲值,對于引用類型則存的是指向?qū)ο蟮囊茫?/p>

  1. 本地方法棧

作用和原理與java棧相似,不同是為執(zhí)行本地方法服務(wù)的。

線程間共享的區(qū)域
1. 堆

大多數(shù)應(yīng)用程序中,堆都是虛擬機所管理內(nèi)存中最大的一塊,唯一目的存放對象的實例和數(shù)組(數(shù)組的引用存放在java棧中)

2. 方法區(qū)

在方法區(qū)中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量以及編譯器編譯后的代碼等

3. 運行時常量池

是方法區(qū)中有一個非常重要的部分,Class文件中除了有類的版本信息、字段、方法、接口等描述信息外,還有一項信息就是常量池。它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM后,對應(yīng)的運行時常量池就被創(chuàng)建出來。當(dāng)然并非Class文件常量池中的內(nèi)容才能進(jìn)入運行時常量池,在運行期間也可將新的常量放入運行時常量池中

對象創(chuàng)建過程

虛擬機上運到new指令時創(chuàng)建對象的步驟:

  1. 類加載檢查
    去檢查這個指令的參數(shù)能否在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載、解析和初始化,如果沒有,那么必須先執(zhí)行類的初始化過程
  2. 虛擬機為新生對象分配內(nèi)存。對象所需內(nèi)存大小在類加載完成后便可以完全確定。
  3. 內(nèi)存分配結(jié)束,虛擬機將分配到的內(nèi)存空間都初始化為零值(不包括對象頭)
  4. 對對象進(jìn)行必要的設(shè)置,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息,這些信息存放在對象的對象頭中
  5. 執(zhí)行<init>方法,把對象按照程序員的意愿進(jìn)行初始化,這樣一個真正可用的對象才算完全產(chǎn)生出來

怎么保證new對象的安全

虛擬機采用了CAS配上失敗重試的方式保證更新更新操作的原子性和TLAB(Thread Local Allocation Buffer,即線程本地分配緩存區(qū))

TLAB這是一個線程專用的內(nèi)存分配區(qū)域。
由于對象一般會分配在堆上,而堆是全局共享的。因此在同一時間,可能會有多個線程在堆上申請空間。因此,每次對象分配都必須要進(jìn)行同步(虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性),而在競爭激烈的場合分配的效率又會進(jìn)一步下降。JVM使用TLAB來避免多線程沖突,在給對象分配內(nèi)存時,每個線程使用自己的TLAB,這樣可以避免線程同步,提高了對象分配的效率。

對象內(nèi)存分配的兩種方法

為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。

指針碰撞(Serial、ParNew等帶Compact過程的收集器)
假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內(nèi)存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。

空閑列表(CMS這種基于Mark-Sweep算法的收集器)
如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯,那就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機就必須維護(hù)一個列表,記錄上哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。

GC回收機制

1. 那些內(nèi)存需要回收

堆是Java虛擬機進(jìn)行垃圾回收的主要場所,其次要場所是方法區(qū)

2. 什么時候回收

2.1 對象在新生代Eden區(qū)中分配,當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時,虛擬機將發(fā)起一次Minor GC。

2.2. 當(dāng)老年代中沒有足夠的內(nèi)存空間來存放對象時,虛擬機會發(fā)起一次Major GC/Full GC,只要老年代的連續(xù)空間大于新生代對象總大小或者歷次晉升的平均大小就會進(jìn)行Minor GC,否則將進(jìn)行Full CG。

2.3. 手動調(diào)用System.gc()方法,通常這樣會觸發(fā)一次的Full GC以及至少一次的Minor GC.

3. 對什么回收,如何找到這些需要回收的垃圾
3.1 堆回收
  1. 引用計數(shù)法 :Java中卻沒有使用這種算法,因為這種算法很難解決對象之間相互引用的情況。

  2. 可達(dá)性分析法:通過一系列稱為“GC Roots”的對象作為起始點,從這些節(jié)點向下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達(dá))時,則證明此對象是不可用的。

Java語言中可以作為GC Roots的對象,只有引用類型的變量才被認(rèn)為是根,值類型的變量永遠(yuǎn)不被認(rèn)為是根包括:
a. 虛擬機棧中引用的對象
b. 方法區(qū)中靜態(tài)屬性引用的對象
c. 方法區(qū)中常量引用的對象
d.本地方法棧中JNI(即Native方法)引用的對象

3.2 方法區(qū)回收

虛擬機規(guī)范中不要求方法區(qū)一定要實現(xiàn)垃圾回收,而且方法區(qū)中進(jìn)行垃圾回收的效率也確實比較低,但是HotSpot對方法區(qū)也是進(jìn)行回收的,主要回收的是廢棄常量和無用的類兩部分。判斷一個常量是否“廢棄常量”比較簡單,只要當(dāng)前系統(tǒng)中沒有任何一處引用該常量就好了,但是要判定一個類是否“無用的類”條件就要苛刻很多,類需要同時滿足以下三個條件:

1、該類所有實例都已經(jīng)被回收,也就是說Java堆中不存在該類的任何實例

2、加載該類的ClassLoader已經(jīng)被回收

3、該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

在大量使用反射、動態(tài)代理、CGLib等ByteCode框架、動態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載功能,以保證方法區(qū)不會溢出。

4. 垃圾回收算法

4.1 標(biāo)記-清除(Mark-Sweep)算法

這是最基礎(chǔ)的算法,標(biāo)記-清除算法就如同它的名字樣,分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收的對象,標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。這種算法的不足主要體現(xiàn)在效率和空間,從效率的角度講,標(biāo)記和清除兩個過程的效率都不高;從空間的角度講,標(biāo)記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片, 內(nèi)存碎片太多可能會導(dǎo)致以后程序運行過程中在需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)一次垃圾收集動作

4.2 復(fù)制(Copying)算法

復(fù)制算法是為了解決效率問題而出現(xiàn)的,它將可用的內(nèi)存分為兩塊,每次只用其中一塊,當(dāng)這一塊內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已經(jīng)使用過的內(nèi)存空間一次性清理掉。這樣每次只需要對整個半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時也不需要考慮內(nèi)存碎片等復(fù)雜情況,只需要移動指針,按照順序分配即可.

新生代的內(nèi)存被劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。每次回收時,將Eden和Survivor中還存活著的對象一次性復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認(rèn)Eden區(qū)和Survivor區(qū)的比例為8:1,意思是每次新生代中可用內(nèi)存空間為整個新生代容量的90%。當(dāng)然,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當(dāng)Survivor空間不夠用時,需要依賴?yán)夏甏M(jìn)行分配擔(dān)保(Handle Promotion)

4.3 標(biāo)記-整理(Mark-Compact)算法

過程與標(biāo)記-清除算法一樣,不過不是直接對可回收對象進(jìn)行清理,而是讓所有存活對象都向一端移動,然后直接清理掉邊界以外的內(nèi)存

GC實現(xiàn)機制-Java虛擬機具體實現(xiàn)流程

a虛擬機在進(jìn)行垃圾回收時,將Eden和Survivor中還存活著的對象進(jìn)行一次性地復(fù)制到另一塊Survivor空間上,直到其兩個區(qū)域中對象被回收完成,當(dāng)Survivor空間不夠用時,需要依賴其他老年代的內(nèi)存進(jìn)行分配擔(dān)保。當(dāng)另外一塊Survivor中沒有足夠的空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔(dān)保機制進(jìn)入老生代,在老生代中不僅存放著這一種類型的對象,還存放著大對象(需要很多連續(xù)的內(nèi)存的對象),當(dāng)Java程序運行時,如果遇到大對象將會被直接存放到老生代中,長期存活的對象也會直接進(jìn)入老年代。如果老生代的空間也被占滿,當(dāng)來自新生代的對象再次請求進(jìn)入老生代時就會報OutOfMemory異常。

6 GC判斷對象死亡過程

Java虛擬機在進(jìn)行死亡對象判定時,會經(jīng)歷兩個過程。如果對象在進(jìn)行可達(dá)性分析后沒有與GC Roots相關(guān)聯(lián)的引用鏈,則該對象會被JVM進(jìn)行第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法,如果當(dāng)前對象沒有覆蓋該方法,或者finalize方法已經(jīng)被JVM調(diào)用過都會被虛擬機判定為“沒有必要執(zhí)行”。如果該對象被判定為沒有必要執(zhí)行,那么該對象將會被放置在一個叫做F-Queue的隊列當(dāng)中,并在稍后由一個虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行它,在執(zhí)行過程中JVM可能不會等待該線程執(zhí)行完畢,因為如果一個對象在finalize方法中執(zhí)行緩慢,或者發(fā)生死循環(huán),將很有可能導(dǎo)致F-Queue隊列中其他對象永久處于等待狀態(tài),甚至導(dǎo)致整個內(nèi)存回收系統(tǒng)崩潰。如果在finalize方法中該對象重新與引用鏈上的任何一個對象建立了關(guān)聯(lián),即該對象連上了任何一個對象的引用鏈,例如this關(guān)鍵字,那么該對象就會逃脫垃圾回收系統(tǒng);如果該對象在finalize方法中沒有與任何一個對象進(jìn)行關(guān)聯(lián)操作,那么該對象會被虛擬機進(jìn)行第二次標(biāo)記,該對象就會被垃圾回收系統(tǒng)回收。值得注意的是finaliza方法JVM系統(tǒng)只會自動調(diào)用一次,如果對象面臨下一次回收,它的finalize方法不會被再次執(zhí)行。

7.內(nèi)存分配 主要規(guī)律

7.1. 本地線程分配緩沖
每個線程在Java堆中預(yù)先分配一小塊內(nèi)存,TLAB比較小,直接在TLAB上分配內(nèi)存的方式稱為快速分配方式,而TLAB大小不夠,導(dǎo)致內(nèi)存被分配在Eden區(qū)的內(nèi)存分配方式稱為慢速分配方式。

7.2. 對象優(yōu)先分配在Eden區(qū)上

7.3. 大對象直接進(jìn)入老年代

7.4. 長期存活的對象將進(jìn)入老年代。Eden區(qū)中的對象在一次Minor GC后沒有被回收,則對象年齡+1,當(dāng)對象年齡達(dá)到“-XX:MaxTenuringThreshold”設(shè)置的值的時候,對象就會被晉升到老年代中。

7.5. Survivor空間中相同年齡的所有對象大小總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代,無須等到“-XX:MaxTenuringThreshold”設(shè)置要求的年齡

8 Java內(nèi)存模型

主內(nèi)存和工作內(nèi)存

Java內(nèi)存模型的主要目的是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。注意一下,此處的變量并不包括局部變量與方法參數(shù),因為它們是線程私有的,不會被共享,自然也不會存在競爭,此處的變量應(yīng)該是實例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素。

Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存(Main Memory)中,每條線程還有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中保存了被該線程使用到的變量和主內(nèi)存副本拷貝(是一些在線程中用到的對象中的字段罷了),線程對變量所有的操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成

內(nèi)存間相互交互

關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存之類的實現(xiàn)細(xì)節(jié),Java內(nèi)存模型中定義了以下8種操作來完成,虛擬機實現(xiàn)時必須保證下面體積的每一種操作都是原子的、不可再分的:

1、lock(鎖定):作用于主內(nèi)存中的變量,它把一個變量標(biāo)識為一條線程獨占的狀態(tài)

2、unlock(解鎖):作用于主內(nèi)存中的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定

3、read(讀取):作用于主內(nèi)存中的變量,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用

4、load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中

5、use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎,沒當(dāng)虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作

6、assign(賦值):作用于工作內(nèi)存中的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存中的變量,每當(dāng)虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作

7、store(存儲):作用于工作內(nèi)存中的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中,以便隨后的write操作使用

8、write(寫入):作用于主內(nèi)存中的變量,它把store操作從工作內(nèi)存中得到的變量值放入主內(nèi)存的變量中

Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時必須滿足以下規(guī)則:

1、不允許read和load、store和write操作之一單獨出現(xiàn)

2、不允許一個線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變了滯后必須把該變化同步回主內(nèi)存

3、不允許一個線程無原因地把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中

4、一個新的變量只能從主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量

5、一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖

6、如果對同一個變量執(zhí)行l(wèi)ock操作,那將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值

7、如果一個變量事先沒有被lock操作鎖定,那就不允許對它進(jìn)行unlock操作,也不允許去unlock一個被其他線程鎖定的變量

8、對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中

volatile型變量的特殊規(guī)則

1、在工作內(nèi)存中,每次使用某個變量的時候都必須線從主內(nèi)存刷新最新的值,用于保證能看見其他線程對該變量所做的修改之后的值

2、在工作內(nèi)存中,每次修改完某個變量后都必須立刻同步回主內(nèi)存中,用于保證其他線程能夠看見自己對該變量所做的修改

3、volatile修飾的變量不會被指令重排序優(yōu)化,保證代碼的執(zhí)行順序與程序順序相同

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容