Java虛擬機之內存管理與GC機制《JAVA虛擬機》要點精煉


參考鏈接:
1.java對象的四種引用:強引用、軟引用、弱引用和虛引用
2.要點提煉| 理解JVM之GC&內存分配
3要點提煉| 理解JVM之類文件結構
4.JAVA中的棧和堆

Java內存管理機制

內存區域劃分

  • JVM執行java程序的過程:首先將.java文件編譯成.class文件,再由JVM中的類加載器加載各個類的字節碼文件,加載完畢之後,交由JVM引擎執行。

JVM會用一段空間來存儲程序運行過程中所需要的數據和相關信息,這段空間叫做運行時數據區。JVM會把它所管理的內存劃分爲若干個不同的數據區域,如下圖。
在這裏插入圖片描述
運行時數據區分成兩類:
(1)線程私有數據區:虛擬機棧、本地方法棧、程序計數器;
(2)線程共享數據區:堆、方法區。

1.程序計算器(Program Counter Register)

它可以看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時,就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

  • Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何情況下,同一時刻只會執行一個線程的的指令,爲了讓線程切換後能到達正確位置,所以每個線程都有自己的程序計算器。
  • 如果線程正在執行的是一個Java方法,那麼計數器記錄的是正在執行的虛擬機字節碼指令的地址
  • 如果線程正在執行的是一個Native方法,那麼計數器的值則爲空

此內存區域是唯一一塊沒有規定任何OutOfMemoryError情況的區域。

2.Java虛擬機棧

虛擬機棧描述的是java方法執行的內存模型

  • 每個方法在執行時,都會創建一個棧幀,其中包括局部變量表,操作數棧、動態鏈接、方法出口等信息。
  • 每一個方法從調用到結束的過程,就對應這一個棧幀從虛擬機棧入棧到出棧的過程。

局部變量表存儲着(1)基本數據類型(int,boolean,float,char,double…)(2)對象引用(refrence)可能是直接指向對象的起始位置的指針也可能是指向一個代表對象的句柄,具體根據訪問定位。(3)returnAddress類型。

特點:

  • 線程內私有
  • 存在兩個異常
    • StackOverFlow異常:如果線程請求的棧深度超過虛擬機棧的深度
    • OutOfMemoryError異常:虛擬機在動態擴展時,如果無法申請到足夠的內存,會拋出異常

3.本地方法棧

與java虛擬機棧相似,只不過java虛擬機棧是爲java方法服務,而本地方法棧是爲本地方法服務,同樣會拋出StackOverFlow與OutOfMemoryError異常。

4.java堆

  • java堆用於存放所有的對象實例和數組
  • 是java虛擬機所管理的內存最大的一塊。
  • 被所有線程所共用。

java堆是垃圾回收器管理的主要區域

  • 細分有新生代老年代
  • 在具體分的話,Eden空間、To Survivor空間、From Survivor空間(應用於新生代區域的GC複製回收算法
  • java堆可以分成多個線程私有的分配緩衝區(Thread Local Allocation Buffer TLAB

除此之外,還有:

  • 在存儲時可以物理上不連續,邏輯上連續即可
  • 會拋出OOM異常,當堆中沒有內存完成實例分配,並且堆也無法再擴展時

5.方法區

  • 用於存儲被虛擬機加載的類型信息常量靜態變量等。(其中類型信息是指:類名,父類名,方法名等等)
  • 與Java堆類似被所有線程所共有
  • 又名永久代
  • 與Java堆相同,可以不選擇連續存儲,或選擇固定大小存儲,可擴展;除此之外還可以選擇不實現GC。
  • 當方法區無法滿足內存分配時,則會拋出OOM異常。

6.運行時常量池

用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

  • 是方法區的一部分
  • 動態性,不要求常量一定只有編譯期才能產生,也就是說並非只有預置在class文件常量池中的常量才能進入運行時常量池,而在運行期間也可以將新的常量放入運行時常量池
  • 同樣具有OOM異常。

虛擬機對象探祕

對象創建

以new操作爲例。

(1)類加載檢查:檢查該指令的的參數是否能在常量池中找到一個類的符號引用,同時這個符號引用所代表的類是否完成了加載解析初始化;沒有則將進行類加載
(2)分配內存:由虛擬機爲對象分配內存,等同於把一塊確定大小的內存從java堆中劃分出來

分配內存一共有兩種方式,根據堆中內存是否規整來判斷使用哪一種方式。

  • 如果內存規整(用過沒用過的內存分別在兩邊
    規整的意思是所有用過的內存放在一邊,所有沒用過的放在另一邊,中間放着指針,可以通過移動指針來給新對象分配內存,只要將指針向沒用過的方法移動與對象長度相同的大小即可。這種方式叫做碰撞指針
  • 如果內存不完整(用過沒用過的混在一起)
    如果用過的內存與沒用過的內存混在一起,虛擬機需要維護一個列表,記錄哪些內存是可以使用的。在給對象分配內存時,要到列表中找到一個比當前對象長度大的位置區存放對象實例。這種方式叫空閒列表。

除了劃分空間外,還有一點是要保證線程安全。(對象創建在虛擬機中十分頻繁,可能出現正在給對象A分配內存, 指針還沒來得及修改, 對象B又同時使用了原來的指針來分配內存的情況)同樣有兩種方式解決線程安全問題。

  • 對內存分配的動作進行同步處理
  • 把內存分配的動作按照線程劃分在不同的空間執行,也就是不同線程在堆中預先分配了自己的緩衝區(Thread Local Allocation Buffer TLAB),並在自己的緩衝區上分配。當TLAB用完需要分配新的TLAB時,在進行同步操作

內存分配完成後, 虛擬機需要將分配到的內存空間都初始化爲零值( 不包括對象頭),這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用, 程序能訪問到這些字段的數據類型所對應的零值。

(3)對象頭的設置,例如:該對象是哪個類的實例找到類的元數據信息的方式對象的哈希碼對象的GC分代年齡等信息存放在對象的對象頭中。

(4)最後執行<init>操作,把對象按照程序員的意願進行初始化。

對象的內存佈局

對象在內存中,存儲的佈局包括:對象頭、實例數據、對齊填充

對象頭中的數據包括兩部分:

  • 存儲對象自身的運行時數據:HashCode、GC分代年齡等等;
  • 類型指針:根據該類型指針就可以知道對象所屬的類。但不是每個對象都需要有類型指針。

實例數據:對象真正存儲的有效信息

對其填充:僅僅起到佔位符的作用,虛擬機中要求對象的大小必須是8的整數倍,對其填充是用於補齊的,

對象的訪問方式

1.使用句柄訪問,java堆會劃分出一塊內存作爲句柄池。reference(對象)就是指向了代表對象的句柄池,句柄池包括對象實例數據的指針與對象類型數據的指針。
在這裏插入圖片描述
2.直接引用
refrence中存儲的就是對象的地址,而java堆中就必須考慮如何放置訪問類型數據的相關信息。
在這裏插入圖片描述
優劣比較:refrence存放的是穩定的句柄,不需要改變,當對象被移動時,只需要更改句柄池中的實例對象指針即可。

使用直接指針訪問方式的最大好處就是速度更快, 它節省了一次指針定位的時間開銷。

垃圾收集器與內存分配

判斷對象是否死亡

引用計數法與可達性分析

1.引用計數方法
使用計數器,每當對象被引用就計算+1,當引用失效就-1,當引用爲0時就證明該對象死亡。但問題在於兩個對象有字段在互相引用,之後將對象置爲null,此時對象其實已經死亡,但計算器仍然不爲1,如下圖。
在這裏插入圖片描述
2.可達性分析算法
通過一系列被稱爲GC Roots的節點,向下尋找,搜索所走過的路叫做引用鏈如果一個對象與GC roots沒有一條引用鏈相連,則判斷該對象死亡。

在這裏插入圖片描述
java虛擬機中可以作爲GC ROOTs的節點包括:

  • 虛擬機棧中棧幀中的本地變量表引用的對象;
  • 方法區中常量引用的對象;
  • 方法區中的靜態屬性引用的對象;

四種引用

java中對象存在四種引用類型。

  1. 強引用:諸如:Object obj = new Object(); ,強引用的對象不會被GC回收
  2. 軟引用:有用但是不是必要的;會在內存空間不足的時候被GC,如果回收之後內存仍不足,纔會拋出OOM異常
  3. 弱引用,與軟引用類似,但是優先級低於軟引用,不管內存是否夠用,在GC時都會被直接回收;
  4. 虛引用僅持有虛引用的對象,在任何時候都可能被GC;作用在於可以當對象回收時,會返回一個信息。

finalize方法

判斷對象是否死亡,**不會只通過可達性分析,而是還會根據是否可以調用finalize方法。**如果可達性分析無法到達GC Node,且不可以調用finalize,纔是真正的死亡。

是否可以調用finalize方法由以下兩方面決定:

  • jvm是否調用過finalize方法
  • 是否實現finalize方法

在finalize中可以實現自救,只要有任何引用鏈中的對象引用了該對象即可,這樣就自救成功,但只可以調用一次finalize方法。

GC方法

我們討論的時堆中的垃圾收集算法。

1.標記-清除算法

標記出哪些對象需要刪除,之後回收所有被標記的對象。
(1)效率較低,標記與清除浪費時間。
(2)這種算法會導致大量的空間碎片,存儲對象的內存和未存儲對象的內存就變得連續在一起了,會導致之後存儲大對象時,會因爲找不到一塊可以存放的內存,而再次GC。
在這裏插入圖片描述

2.複製算法

將內存等分成兩部分,當一部分存儲滿了之後,就將該部分中存活的對象都複製到另一部分中,然後將該部分的內存全部清除。這樣做的好處就是沒有了空間碎片,但每次使用的內存只有50%

應對該算法的改進是由於新生代的對象存活時間短,因此將內存區分成Eden,兩個Survrior,比例是8:1:1,每次使用90%的內存去存儲。當回收時,會將他們其中的存活對象放入另一個Survrior中,最後清理掉Eden與剛剛用過的Surviror內存。
在這裏插入圖片描述

3.標記-整理算法

首先標記需要刪除的對象,之後將所有存活的對象都移動到一邊,然後對邊界外的內存進行清理。
在這裏插入圖片描述

4.分代算法

分代算法將標記-移動算法與複製算法相結合,根據對象存活週期的不同,將Java堆劃分爲新生代和老年代,並根據各個年代的特點採用最適當的收集算法。

  • 新生代中對象大量死去,因此則使用複製算法
  • 老年代中對象存活率高,因此使用標記-整理算法標記-清除算法

HotSpot的算法實現

主要包括瞭如何枚舉根節點,以便進行判斷對象是否存活,以及該在什麼地點或區域進行GC。

1.枚舉根節點

GCRoot一般在全局性引用(常量或靜態屬性)或上下文(楨棧中的本地變量表)的引用位置,可達性分析對時間的敏感主要體現在GC停頓,而GC停頓主要就在枚舉根節點上。

虛擬機中一般使用準確式GC,可以得知所有全局和上下文的引用位置,在HotSpot中是使用OopMap實現的該功能。完成類加載後會計算出對象某偏移量上某類型數據,**JIT編譯時會在特定的位置記錄棧和寄存器中是引用的位置。**這樣GC在掃描時就可直接得知這些信息,並快速準確地完成GC Roots的枚舉。

2.安全點

JIT並不會在所有位置都記錄,而只是會在特點的位置記錄,這個記錄的位置叫做安全點。程序只會在安全點之後暫停並進行GC。安全點設置不能太少或太多,而選擇的標準爲是否具有讓程序長時間執行的特徵如方法調用、循環跳轉、異常跳轉。

當GC發生時,有兩種方法使所有線程都走到中斷點
(1)搶先式中斷不需要代碼配合,立即停止所有線程,如果有線程沒有走到中斷點,則開啓該線程讓線程走到中斷點。
(2)主動式中斷設置一個標記位,各個線程執行時輪詢該標誌位,如果爲真則自己主動掛起,標記位與中斷點重合。

3.安全區域

上述安全點是運行在線程運行的狀態下,如果是線程不運行就不可以了。而安全區域是在區域內都可以進行GC。

安全區域是指在一段代碼片段之中, 引用關係不會發生變化。 在這個區域中的任意地方開始GC都是安全的。

當線程運行到Safe Region中時,會將線程標記爲Safe Region,這樣的線程在JVM發起GC時就不會處理。當線程從Safe Region中離開時會判斷是否完成GC,完成則會繼續執行其他操作,否則就要執行。

主要的垃圾回收器

在這裏插入圖片描述
並行是指:多條線程同時進行GC,但用戶線程是停止的;
併發是指:GC線程與用戶線程是同時或有順序交替執行的;

內存分配與回收策略

自動內存管理是指:給對象自動分配內存並自動回收不需要的對象的內存。

對象的分配是指在堆上的分配,對象主要分配在新生代的Eden區中,如果啓動了線程本地內存緩衝,則優先存放在TLAB中。最後則存放在老年代中。

內存分配遵循以下幾條規則。

  • 1.對象優先在新生代的Eden中存放
    大多數情況下, 對象在新生代Eden區中分配。 當Eden區沒有足夠空間進行分配時, 虛擬機將發起一次MinorGC。
    MinorGC指新生代區發生的GC,一般速度較快;
    FullGC指老年代內存發生的GC,速度會比MinorGC慢上10倍;

  • 2.大對象直接進入老年代
    可以設置屬性PretenureSizeThreshold,當對象大小大於一定時,直接放入老年代。這樣做的目的是防止Eden區與Survivor區發生大量的內存複製。

  • 3.長期存活的對象直接放入老年代
    由於Minor GC經常發生,我們會在將存活對象放入Survior的同時,將其age增1。我們可以通過設置屬性MaxTenuringThreshold(默認爲15),當age大於多少時可以將對象放入老年代。

  • 4.動態年齡判斷
    存活年齡相同的對象的大小之後如果大於當前Survivor容量的一半則將大於等於該對象年齡的對象放入老年代。

  • 5.空間分配擔保
    當Minor GC之後,會將仍然存活的對象放入Survivor中,如果Survivor存儲不下的話,就會挪用老年代的空間。
    因此, 在MinorGC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間, 如果這個條件成立, 那麼Minor GC可以確保是安全的,否則會判斷老年代是否進行擔保(HandlePromotionFailure)。
    如果擔保,則會根據平均分配到存活對象來比較老年代所剩餘的空間,如果可以存放,則執行一次冒險的MinorGC ,如果不夠存放,則會直接進行一次Full GC。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章