1. 程式人生 > >JVM理解-記憶體管理

JVM理解-記憶體管理

JVM理解 —— 記憶體管理

Java不是由開發人員來顯示分配記憶體和回收記憶體,而是由JVM來 自動管理記憶體的分配和回收(又稱為:垃圾回收Garbage Collection:GC)。這降低了開發的難度,但是實際使用中遇到的問題就是由於不清楚JVM的 記憶體分配 和 回收機制,造成記憶體洩漏,最終導致JVM記憶體不夠用。

1. 記憶體空間

JDK遵照 JVM規範進行記憶體區域的劃分,如下:

  • 程式計數器
  • Java虛擬機器棧
  • 本地方法棧
  • 方法區(非堆)

1.1 程式計數器

資料活動範圍:

  • 當前執行緒私有

說明:

  • 佔用一塊較小的記憶體空間。
  • 可以看做是當前執行緒所執行的 位元組碼 的 行號指示器。

丟擲異常:

Java虛擬機器棧

資料活動範圍:

  • 當前執行緒私有;生命週期與執行緒相同

說明:

  • Java方法執行的記憶體模型:每個方法在執行的同時建立一個棧幀(Stack Frame),用以儲存:

    • 區域性變量表
    • 運算元棧
    • 動態連線
    • 返回方法地址

    每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器中執行 入棧 到 出棧 的過程。

丟擲異常:

  • 執行緒請求的棧深度大於虛擬機器所允許的深度,丟擲:StackOverflowError。
  • 無法申請到足夠記憶體時,丟擲:OutOfMemoryError。

1.2 本地方法棧

資料活動範圍:

  • 當前執行緒私有

說明:

  • 與 虛擬機器棧發揮的作用類似,不同點:
    • 虛擬機器棧為虛擬機器執行Java方法(位元組碼)服務
    • 本地方法棧為虛擬機器執行的 Native方法服務

丟擲異常:

  • StackOverflowError。
  • OutOfMemoryError。

1.3 方法區(非堆)

資料活動範圍:

  • 所有執行緒公有

說明:

  • 儲存已被 虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼。
  • 永久代
  • 執行時常量池,存放編譯期生成的各種
    • 字面量
    • 符號引用
    • 直接引用
    • 具有動態性(相對於常量池)

丟擲異常:

  • OutOfMemoryError。

1.4 堆

資料活動範圍:

  • 所有執行緒公有

說明:

  • 是Java虛擬機器所管理的記憶體中最大的一塊,
  • 唯一目的:存放所有物件例項、陣列
  • 是垃圾收集器管理的主要區域,實行:分代收集演算法(JDK 1.2開始):
    • 新生代:Eden Space,Survivor Space(From Space)+ Survivor Space (To Space)(From:To = 1:1)
    • 舊生代(新:舊 = 1:2)
  • TLAB(Thread Local Allocation Buffer) 執行緒私有緩衝區

丟擲異常:

  • OutOfMemoryError。

1.4.1 新生代(New Generation)

1.4.1.1 新生代記憶體由:

  • Eden Space
  • 2塊相同大小的 Survivor Space 構成,也被稱為:From Space 和 To Space
  • Eden Space 與 From Space 與 To Space 所佔新生代記憶體空間的比值= 8:1:1
  • 新建物件只會存在於 Eden Space 和 From Space 中, To Space 是空的 —— 此種分配方式與 複製回收演算法 有關。

不同的GC方式會以不同的方式來劃分或者根據執行狀態調整 Eden Space 和 Survivor Space 的大小。

大多數情況下Java程式中所有新建的物件都是從 新生代 的記憶體中分配的,當 Eden Space 不足時,會把存活物件轉移動 Survivor Space 中。

新建立的物件一般情況下都會分配到 Eden Space 中(大物件、大陣列分配到 舊生代 中),當物件經過第一次 Minor GC 後(物件年齡+1),如果仍然存活則將存活物件移動至 Survivor Space 中,當物件的年齡達到一定的歲數時,將被移動至 舊生代中。

1.4.1.2 為什麼 新生代中會有 Survivor Space?

  • 如果沒有 Survivor Space , Eden Space 中每進行一次 Minor GC後,存活的物件就會被送到 舊生代 中,這樣就會導致 舊生代 很快被填滿,發生 Major GC或 Full GC。

    但由於 舊生代 記憶體空間遠大於 新生代 空間,所以進行一次 Full GC 會消耗大量的時間。而且若是在無 Survivor Space的情況下,舊生代 被填滿的速度大大加快,結果就是導致 頻繁 的發生 Full GC,這將會影響程式的執行和響應速度,更有甚至會因為 超時 產生其他問題。

    在無 Survivor Space 的情況下,如何解決?

方案 優點 缺點
增加老年代空間 更多存活物件才能填滿老年代。降低Full GC頻率 隨著老年代空間加大,一旦發生Full GC,執行所需要的時間更長
減少老年代空間 Full GC所需時間減少 老年代很快被存活物件填滿,Full GC頻率增加

上述兩種解決方案都不能從根本上解決問題。

結論一:

Survivor Space 存在的意義就是 減少被移動至 舊生代 中的物件,進而減少 Full GC 發生。

1.4.1.3 為什麼 Survivor Space 會有 From Space 與 To Space 2塊空間?

由上文已經說明了為什麼沒有 Survivor Space 不行,這次假設只有 1塊 Survivor Space

當只有 1塊 Survivor Space 時,新建物件填滿了 Eden Space ,此時就會觸發 Minor GC ,然後 Eden Space 中的存活物件被複制至 Survivor Space 中。

這樣的操作一直迴圈下去,雖然存活物件得到了轉移,但是這2個區域中物件所佔用的記憶體地址並不是連續的,這就導致了在只有 1塊Survivor Space 的條件下產生的問題:

1
記憶體地址碎片化

記憶體地址碎片化導致的問題就是:

嚴重影響Java程式的效能,堆空間中被散佈的物件佔據著不連續的記憶體,最直接的結果就是:沒有足夠大的連續空間,記憶體碎片

1.4.1.4 建立 2塊Survivor Space 的意義

當建立 2塊Survivor Space 時:

  • 對已滿的 Eden Space 進行 Minor GC 時,存活物件就會被複制至 From Space 中,Eden Space 被清空。

  • 當對已滿的 Eden Space 再次進行 Minor GC 時,再次執行 Minor GC,Eden Space 和 From Space 中的存活物件將被複制至 To Space 中( ==這種 複製演算法 保證了 To Space 中來自 Eden 和 From Space 2部分中存活物件所佔用的記憶體空間地址是連續的,避免了碎片化的產生 == )。

  • 之後對 Eden Space 和 From Space 記憶體空間進行清空,將 From Space 和 To Space 進行交換即:將 To Space 中存活的物件複製至 From Space 中,保證 To Space 是空的

所以,建立 2塊Survivor Space 的意義就是:

1
永遠有一個 Survivor Space 是空的,另一個非空的 Survivor Space 無碎片。

參考:https://blog.csdn.net/u012799221/article/details/73180509

1.5 舊生代(Old Generation/Tenuring Generation)

  • 是用於存放 新生代 記憶體中經過多次垃圾回收後仍然存活的物件,例如 快取物件
  • 新建的物件也有可能在 舊生代 中直接分配記憶體。主要由2種情況決定:

    • 新建物件為大物件
    • 新建物件為大陣列物件,且陣列中無引用外部物件

2. 記憶體分配

2.1 堆分配

Java物件所佔用的記憶體主要是從  上進行分配, 是 所有執行緒共享 的,因此在堆上分配記憶體時是 需要加鎖 的。這導致了建立物件開銷比較大。當  上的記憶體空間不足時,會觸發 GC ,如過 GC 後記憶體空間仍然不足,則丟擲 OOM

2.2 TLAB(Thread Local Allocation Buffer)分配

JDK 為了提升記憶體分配效率,會為 每個新建立的執行緒 在 新生代的 Eden Space 上分配一塊獨立空間。這塊空間稱為 TLAB(Thread Local Allocation Buffer),其大小是由JVM根據執行情況而得出的。

在 TLAB 中分配記憶體是 不需要加鎖 的,因此JVM在給執行緒中的物件分配記憶體時會盡量在 TLAB 上分配。如果物件過大或者 TLAB 空間已經用完了,則仍然在  上分配記憶體

因此在編寫Java程式時,通常多個 小的物件比大的物件在記憶體分配上 更高效。 


3. 垃圾回收

JVM 通過 GC 來回收  和  中的記憶體。

**GC 的基本原理是**:

  • 首先找到程式中不在被使用的物件
  • 然後回收這些物件所佔用的記憶體

3.1 GC 分類:

Java 中的堆也是 GC 收集垃圾的主要區域。

  • Minor GC - 新生代 - 複製演算法
  • Full GC(又稱 Major GC)- 舊生代 - 標記-清除演算法

3.1.1 Minor GC

 中 新生代 中物件的生命週期(80%)一般為 朝生夕死。所以在 新生代 中使用的垃圾回收演算法是 複製算髮

  • 在GC開始前,物件只會存在於 Eden Space 和 From Space 區,此時 To Space 是空的。

  • 當GC執行時,Eden Space 中存活的物件被複制到 To Space 中;同時 From Space 中的物件根據其年齡值(年齡閾值可以通過-XX:MaxTenuringThreshold來設定)來決定去向:

    • 到達一定值,則物件將移動至 舊生代 ;
    • 未達到一定值,則物件被複制到 To Space 中。
  • 之後 Eden Space 和 From Space 區域已經被清空,此時,將 From Space 和 To Space 的角色進行交換(保證命名為 To Space 是空的)。

  • Minor GC 會一直重複這樣的過程,直到 To Space 被填滿,將所有物件移動至 舊生代 中。

3.1.2 Major GC

Major GC 是發生在 舊生代 的垃圾收集動作,所採用的是 標記-清除 演算法。

舊生代 中的物件有部分是從 新生代 中移動過來的。所以不會輕易被回收掉,因此 Major GC 不會像 Minor GC 那樣發生的頻繁,並且做一次 Major GC 要比 Minor GC 消耗的時間長。

由於使用 標記-清除 演算法時,會導致產生記憶體碎片(即:不連續的記憶體空間),當此後要為較大的物件分配記憶體空間且沒有較大記憶體空間時,會提前觸發一次GC操作。

3.2 回收演算法

回收演算法分類:

  • 引用計數 收集器:判斷物件的 引用數量
  • 可達性 收集器:判斷物件的 引用鏈是否可達

可達性收集器 所涉及的記憶體區域:

  • JVM棧中引用的物件
  • 方法區中類靜態引用的物件
  • 方法區常量引用的物件
  • JNI引用的物件

3.3 引用計數 收集器

引用計數 收集器採用的是 分散式 的管理方式,通過 計數器 記錄是否對 物件進行引用。當 一個物件的引用計數器為0 時,說明此物件已經不在被使用,於是進行回收

不足

  • 引用計數器 需要在每次對物件賦值時進行 計數器的增減,它有一定的消耗

  • 引用計數器 對於 迴圈引用 的場景是 無法實現回收 的。如:物件B 和 物件C 互相引用時,

    對於Java這種面向物件的會形成複雜引用關係的語言而言,引用計數器 是非常不合適的,在 JDK 的 GC 實現中 未採用此種方式。

3.4 可達性 收集器

可達性收集器採用的是 集中式 管理方式。全域性記錄 資料的 引用狀態

基於一定條件(定時、空間不足時)的觸發 ,使用跟蹤收集器,執行時需要從 根集合 來掃描 物件的引用關係 ,這可能會造成 應用程式暫停

實現演算法有3種

  • 複製(Copying)
  • 標記-刪除(Mark-Sweep)
  • 標記-壓縮(Mark-Compact)

3.4.1 複製(Copying)

複製採用的方式是:

  • 從 根集合 掃描出 存活的物件
  • 將 找到的存活物件 複製到 一塊新的完全未使用的空間中

複製特點:

當要回收的空間中 存活的物件較少時,複製演算法會比較高效,但是其帶來的成本是:要 增加一塊 全新的記憶體空間 及進行 物件的 移動

3.4.2 標記-清除(Mark-Sweep)

此種方式採用的是方式:

  • 從 根集合 開始掃描, 對 存活物件進行標記
  • 標記完成之後,再次掃描 整個空間中 未標記的物件,並對其進行回收。

標記-清除 特點:

  • 此種方式 不需要進行物件的移動, 僅對 不存活的物件 進行清除。因此在空間中 存活物件較多 的情況下,較為高效。
  • 由於此方式採用的是 直接回收不存活物件所佔用的記憶體空間,因此會造成 記憶體碎片

3.4.3 標記-壓縮(Mark-Compact)

  • 此種方式採用和 標記-清除 一樣的方式對 存活物件進行標記
  • 在 回收不存活物件所佔用的記憶體空間 後,會將 其他所有存活的物件 都往 左端空閒的空間進行移動,並 更新引用其物件的指標

    標記-壓縮 特點:

  • 需要 對存活物件進行移動,成本相對較高

  • 不產生碎片

參考:https://www.cnblogs.com/sunniest/p/4575144.html

3.5 物件引用強弱問題

引用型別 被垃圾回收時間 用途 生存時間
強引用 從來不會 物件的一般狀態 JVM停止執行時終止
軟引用 在記憶體不足時 物件快取 記憶體不足時終止
弱引用 在垃圾回收時 物件快取 gc執行後終止
虛引用 Unknown 標記的物件被回收後傳送一條通知 Unknown

3.6 物件建立

  1. 虛擬機器接到 New 指令後,先檢查這個 指令的引數 是否能在 常量池 中定位到一個 符號引用,並檢查此符號引用是否經過了載入驗證等一系列步驟。

    若未經過此步驟,那麼 先執行類載入步驟

  2. 類載入檢查通過後,JVM在堆中為新生的物件分配記憶體空間,空間的大小在類載入完成之後就已經得知。分配方式:

    • 若堆中的 記憶體足夠規整,那麼進行 指標碰撞 記憶體分配;
    • 若堆中 記憶體不夠規整,那麼進行 空閒列表記憶體分配。

    分配記憶體空間 結束後,將對物件的進行 實際建立,建立方式:

    • 對分配記憶體空間的動作進行 同步處理
    • 對分配記憶體空間的操作按照 執行緒 劃分在 不同的空間 TLAB 中進行。
  3. 記憶體分配 結束後,JVM要將記憶體空間 初始化為0值

  4. JVM對 物件進行必要的設定。如:此物件是哪個類的例項、如何找打類的元資料資訊、物件的hash值、物件的GC年齡等資訊。

  5. 到此為止,在JVM中一個新的物件產生了,但是 Java中一個物件才剛開始建立,接下來要執行: <init>(),所有欄位都歸0