詳細瞭解JVM執行時記憶體

2022-11-04 15:00:44

詳細瞭解JVM執行時記憶體

1.程式計數器

概念

程式計數器也叫作PC暫存器,是一塊很小的記憶體區域,可以看做是當前執行緒執行的位元組碼的行號指示器。位元組碼的解釋工作就是通過改變程式計數器裡面的值來獲得下一條需要執行位元組碼的指令。

特點

  • Pc暫存器表現為一塊記憶體,功能是存放偽指令,確切的說是存放的將要執行指令的地址。
  • 當虛擬機器器正在執行的是一個native方法時,JVM的PC暫存器儲存的值是undefined。
  • 程式計數器是執行緒私有的,它的生命週期和執行緒一樣,每個執行緒只有一個。這也是為了保證多執行緒下,執行緒切換後能恢復到正確的執行位置,所以每個執行緒需要獨立的程式計數器,相互隔離互不影響。
  • 此區域是唯一一個沒有OOM情況的區域。

圖例

2.虛擬機器器棧

概念

JAVA虛擬機器器棧的生命週期和執行緒相同,他也是執行緒私有的,每一個執行緒有自己獨立的虛擬機器器棧。他用來儲存棧幀,程式執行時,每一個方法被呼叫執行時都會建立一個棧幀,用來儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫到執行完就對應著一個棧幀在虛擬機器器中從入棧到出棧的過程。

圖例演示

棧幀

棧幀是支援虛擬機器器方法呼叫和執行的資料結構。棧幀中儲存了方法的區域性變數表、運算元棧、動態連線和方法返回地址等資訊。每一個方法的呼叫和執行完成都對應著一個棧幀從虛擬機器器棧中入棧到出棧的過程。

設定虛擬機器器棧的大小

-Xss為JVM啟動時的每個執行緒分配的記憶體大小,也就是可以設定執行緒棧的大小。

-Xss1m     # 單位為MB
-Xss1024k  #單位為KB
-Xss1048576  #位元組大小

區域性變數表

區域性變數表是一組變數值儲存空間,用於存放方法的引數和方法內定義的區域性變數。

運算元棧

運算元棧是一個後入先出棧(LIFO)。隨著方法執行和位元組碼指令的執行,會從區域性變數表或者物件範例的欄位中複製常數或者變數寫到運算元棧,再隨著計算的進行會將棧中的元素出棧到區域性變數表或者返回給方法呼叫者。

動態連結

java虛擬機器器中,每一個棧幀都包含一個指向執行時常數池中該棧所屬方法的符號參照,持有這個參照的目的為了支援方法呼叫過程中的動態連結。 動態連結的作用:將符號參照轉換為直接參照。

方法返回地址

方法返回地址存放呼叫該方法的PC暫存器的值。一個方法的結束,有兩種方式:正常地執行完成,出現未處理的異
常非正常的退出。無論通過哪種方式退出,在方法退出後都返回到該方法被呼叫的位置。方法正常退出時,呼叫者
的PC計數器的值作為返回地址,即呼叫該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過
異常表來確定,棧幀中一般不會儲存這部分資訊。
無論方法是否正常完成,都需要返回到方法被呼叫的位置,程式才能繼續進行。

3.本地方法棧

概念

本地方法棧則是為虛擬機器器使用到的本地(Native) 方法服務,而虛擬機器器棧是為使用到的java方法服務。

關於native方法

native關鍵字修飾的Java方法是一個原生態方法,方法對應的實現Java作用範圍達不到,而是在用其他程式語言(如C和C++)檔案中實現。Java語言本身不能直接對作業系統底層進行存取和操作,但可以通過JNI介面呼叫其他程式語言來實現對作業系統底層的存取。 native方法在異地實現,類似抽象方法,不能有方法體,要以分號結束。例如:

本地方法棧特點

  • 本地方法棧載入nativef方法,是為了填補java不方便實現的場景產生的。
  • 虛擬機器器棧為為虛擬機器器執行java服務,而本地方法棧為了執行虛擬機器器所使用到的native服務
  • 本地方法棧也是執行緒私有的,和執行緒的生命週期是一致的,每個執行緒都有一個本地方法棧。

4.堆

4.1 堆的總括

4.1.1 概念

Java堆(Java Heap) 是虛擬機器器所管理的記憶體中最大的一塊。 Java堆是被所 有執行緒共用的一塊記憶體區域, 在虛擬機器器啟動時建立。 此記憶體區域的唯一目的就是存放物件範例, Java 世界裡「幾乎」所有的物件範例都在這裡分配記憶體。

4.1.2 特點

  • 堆是Java虛擬機器器所管理記憶體中最大的一塊區域。
  • 堆是執行緒共用的。
  • 堆在虛擬機器器啟動的時候建立。
  • 堆存在的目的就是存放物件範例。
  • 堆是垃圾回收管理的主要區域。因此堆又被稱作為GC堆,JAVA堆還可以細分為新生代,老年代,永久代(jdk8以後就取消了),其中新生代又分為Eden空間、From survivor、To survivor。
  • 堆在計算機物理上儲存是不連續的,但是邏輯上是連續的,它的大小可以調節(-Xmx,-Xms控制)。
  • 方法結束後,堆物件不會馬上的移除,僅僅在垃圾回收的時候才會移除。
  • 如果堆中沒有足夠的記憶體完成對範例的分配,且堆的空間無法再擴充套件時,那麼將會報出OOM異常。

4.1.3 設定堆記憶體大小

我們可以通過-Xms來設定最小堆記憶體,通過-Xmx設定最大堆記憶體。

以上是設定了:-Xms5m -Xmx20m

這裡可以看出列印出來的Xmx值18m和設定的值20m之間是有差異的,total Memory和最大的記憶體之間也還是存在比較明顯的差異,就是說JVM一般會盡量保持記憶體在一個儘可能底的層面,而非貪婪做法按照最大的記憶體來進行分配。

另外,當我們申請分配記憶體10m時,我們會發現free Memory和total Memory都上升了,可以看出JVM在記憶體分配時是動態分配的。

4.1.4堆的分類

JAVA將虛擬機器器堆分為三個部分:

  • 新生代 (又分為伊甸園區,倖存者區s0和倖存者區s1)
  • 老年代
  • 永久代(JDK1.8後沒有了,被本地記憶體的元空間取代了)

圖例如下:

4.2 新生代和老年代

4.2.1 物件儲存

  • 新生代存放剛建立的範例物件,記憶體比較小,垃圾回收比較頻繁。新生代又分為Eden區,survivor To區S0和survivor From區S1,其中S0區和S1區並不是固定的from及to的區域,由物件轉移的方向決定的,假設物件從S1轉移到S0,那麼S1便是survivor From,S0是survivor To。
  • 老年代主要存放一些生命週期比較長的物件,經過在新生代幾次的回收依舊沒有清除掉,那這部分範例便會轉移到老年代。老年代的垃圾回收相對來講沒有那麼頻繁。

4.2.2 設定新生代和老年代的堆中佔比

預設情況下-XX:NewRatio=2,表示新生代:老年代 = 1:2,新生代佔整個堆空間的1/3

案例:假設我們將-XX:NewRatio修改為等於4,那麼則表示新生代:老年代 = 1:4,那麼新生代佔整個堆空間的1/5

除了我們可以設定新生代和老年代的比例之外,我們還可以設定eden和S0和S1在新生代中的佔比情況,預設情況下-XX:SurvivorRatio = 8,表示Eden:S0:S1=8:1:1,這表示Eden佔整個新生代的8/10,而兩個survivor區域分別佔了1/10,另外,需要補充一點,由於JVM在執行時,每次都只會使用Eden區和一塊survivor區進行服務,因此總是會有一個survivor區域是空閒著的,所以新生代的最高使用也只能達到9/10。

4.3 物件分配過程

  • new物件時首先會將物件放在eden區,該區大小有記憶體限制。
  • 當eden區的資料滿了之後,程式還需要建立物件,會觸發垃圾回收,將那些不再被參照的物件給銷燬掉。
  • 剩餘沒被回收掉的物件會被轉移到S0區,而程式新建立的物件又會繼續寫入Eden區。
  • 當再次發生垃圾回收時,如果S0中還存在未被銷燬的物件,那麼這部分剩餘的物件會被轉移到S1中。
  • 之後每次經歷垃圾回收,存在S0或者S1中未被銷燬的物件總會相互轉移過去。
  • 當這種轉移達到15次上限後,那麼這部分物件將會被轉移到老年區。當然這個閾值並不是固定15,可以通過調節引數 -XX:MaxTenuringThreshold=N來控制閾值。
  • 當養老區的記憶體也不足時,會觸發GC進行養老區的垃圾回收。
  • 如果養老區進行了GC垃圾回收後還是沒有辦法儲存新建立的物件,那麼將會報OOM異常。

4.4 堆GC

Java中的堆是虛擬機器器中GC收集垃圾的主要區域。GC分為兩種,一種是部分收集(Partial GC),一種是整堆收集(Full GC).

部分收集

  • 新生代收集(Minor GC/Yong GC):只是新生代的垃圾回收。
  • 老年代收集(Major GC/Old GC):只是老年代的垃圾回收。
  • 混合收集(Mixed) :收集整個新生代和老年的垃圾。(G1 GC會混合回收, region區域回收)

整堆收集(Full GC):收集整個java堆和方法區的垃圾收集器

年輕代GC觸發條件

  • 當年輕代記憶體不足時,會觸發Minor GC,這裡的記憶體不足指的是Eden區的記憶體不足,Survivor區不會。
  • Minor GC 會暫停其他使用者的執行緒,等到垃圾回收結束,使用者的執行緒才恢復。

老年代GC觸發條件

  • 老年代空間不足時,會嘗試觸發Minor Gc,如果空間還是不足,則會觸發Major GC
  • 如果Major GC結束後,空間還是不足,會報OOM異常。
  • Major GC的速度比Minor GC慢10倍以上。

Full GC觸發條件

  • 程式呼叫System.gc(),會觸發Full GC,但不會立即去執行。
  • 老年代空間不足。
  • 方法區空間不足。
  • 通過Minor GC後仍能進入老年代的物件所佔空間大於老年代剩餘可用空間。

5.元空間

JDK1.8後為什麼廢除永久代,引入元空間

  • 在之前的永久代中,它是堆的一部分,主要是在儲存類的後設資料、靜態變數、常數等,這些資料的大小也不太容易控制和計算,開發人員對永久代進行調優會有很多的難度。永久代會對GC帶來不必要的複雜度,回收效率偏低。
  • 而用元空間替代永久代,這樣的話可以很好的解決這個問題,因為元空間是放在本地記憶體上的,簡而言之,只要你伺服器記憶體還有,元空間基本就不會發生記憶體溢位等問題。

廢除永久代的好處

  • 由於類的後設資料分配在本地記憶體上,這樣就說元空間的最大分配記憶體就是伺服器系統剩餘可用記憶體,不會遇到永久代時存在的記憶體溢位問題。
  • 將執行時常數池從永久代中分離出來,與類的後設資料分開,提高了後設資料的獨立性。
  • 將後設資料從永久代剝離出來放到元空間,可以提升對後設資料的管理,同時也提升GC效率。

元空間相關引數

  • -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行型別解除安裝,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
  • -XX:MaxMetaspaceSize,最大空間,預設是沒有限制的。如果沒有使用該引數來設定類的後設資料的大小,其最大可利用空間是整個系統記憶體的可用空間。JVM也可以增加本地記憶體空間來滿足類後設資料資訊的儲存。但是如果沒有設定最大值,則可能存在bug導致Metaspace的空間在不停的擴充套件,會導致機器的記憶體不足;進而可能出現swap記憶體被耗盡;最終導致程序直接被系統直接kill掉。如果設定了該引數,當Metaspace剩餘空間不足,會丟擲:java.lang.OutOfMemoryError: Metaspace space。
  • -XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為分配空間所導致的垃圾收集。
  • -XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導致的垃圾收集。

6.方法區

6.1方法區的理解

概念:

元空間、永久代是方法區具體的落地實現。方法區看作是一塊獨立於Java堆的記憶體空間,它主要是用來儲存所載入
的類資訊的,方法區是執行緒共用的。

特點:

  • 方法區與堆一樣是各個執行緒共用的記憶體區域。
  • 方法區在JVM啟動的時候就會被建立,並且它實際的實體記憶體空間和Java堆一樣可以是不連續的。
  • 方法區的大小跟堆一樣,可以選擇固定的大小或者動態變化。
  • 方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器器仍然會報OOM異常。
  • 關閉虛擬機器器就會釋放方法區區域。

6.2 方法區結構

類載入器將Class檔案載入到記憶體以後,將類的資訊儲存到方法區中。

方法區中儲存的內容:

  • 型別資訊(域資訊、方法資訊)
  • 執行時常數池

型別資訊

  • 這個型別的完整有效名稱(全名 = 包名.類名)
  • 這個型別直接父類別的完整有效名(對於 interface或是java.lang. Object,都沒有父類別)
  • 這個型別的修飾符( public, abstract,final的某個子集)
  • 這個型別直接介面的一個有序列表

域資訊

域資訊,即為類的屬性,成員變數
JVM必須在方法區中儲存類所有的成員變數相關資訊及宣告順序。
域的相關資訊包括:域名稱、域型別、域修飾符(pυblic、private、protected、static、final、volatile、transient的
某個子集)

方法資訊

  1. 方法名稱方法的返回型別(或void)
  2. 方法引數的數量和型別(按順序)
  3. 方法的修飾符public、private、protected、static、final、synchronized、native,、abstract的一個子集
  4. 方法的位元組碼bytecodes、運算元棧、區域性變數表及大小( abstract和native方法除外)
  5. 異常表( abstract和 native方法除外)。每個例外處理的開始位置、結束位置、程式碼處理在程式計數器中的偏
    移地址、被捕獲的異常類的常數池索引

6.3 方法區設定

方法區的大小不必是固定的,可以根據應用的需要動態調整

  • jdk7及之前
    • 通過-xx:Permsize來設定永久代初始分配空間。
    • -XX:MaxPermsize來設定永久代最大可分配空間。64位元的機器預設是82M。當JVM載入的類資訊容量超過了這個值,會報OOM異常:PermGen space。
  • jdk8及以後
    • 後設資料區大小可以使用引數 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定。但是後設資料區的 -XX:MaxMetaspaceSize預設是-1即沒有限制,不設定可以使用系統剩餘所有記憶體。
    • 如果後設資料區發生溢位,虛擬機器器一樣會丟擲異常OutOfMemoryError:Metaspace

7.執行時常數池

位元組碼檔案中,內部包含了常數池

方法區中,內部包含了執行時常數池

常數池:存放了編譯期間產生的各種字面量和符號參照。

執行時常數池:是常數池表在執行時的一種表現形式。

編譯後的位元組碼檔案中包含了型別資訊、域資訊、方法資訊等。通過ClassLoader將位元組碼檔案的常數池中的資訊載入到記憶體中,儲存在了方法區的執行時常數池中。

常數池,可以看做是一張表,虛擬機器器指令根據這張常數表找到要執行的類名、方法名、引數型別、字面量等型別。