1. 程式人生 > 其它 >6.堆和GC關係 及 本地jvm調優

6.堆和GC關係 及 本地jvm調優

一. 堆和GC介紹

1.java堆的特點

《深入理解java虛擬機器》是怎麼描述java堆的

  • Java堆(Java Heap)是java虛擬機器所管理的記憶體中最大的一塊
  • java堆被所有執行緒共享的一塊記憶體區域
  • 虛擬機器啟動時建立java堆
  • java堆的唯一目的就是存放物件例項。
  • java堆是垃圾收集器管理的主要區域。
  • 從記憶體回收的角度來看, 由於現在收集器基本都採用分代收集演算法, 所以Java堆可以細分為:新生代(Young)和老年代(Old)。 新生代又被劃分為三個區域Eden、From Survivor, To Survivor等。無論怎麼劃分,最終儲存的都是例項物件, 進一步劃分的目的是為了更好的回收記憶體, 或者更快的分配記憶體。
  • java堆的大小是可擴充套件的, 通過-Xmx和-Xms控制。
    • 如果堆記憶體不夠分配例項物件, 並且堆也無法在擴充套件時, 將會丟擲outOfMemoryError異常。

2.堆記憶體劃分:

  • 堆大小 = 新生代 + 老年代。堆的大小可通過引數–Xms(堆的初始容量)、-Xmx(堆的最大容量) 來指定。
  • 其中,新生代 ( Young ) 被細分為 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名為 from 和 to,以示區分。預設的,Edem : from : to = 8 : 1 : 1 。(可以通過引數 –XX:SurvivorRatio 來設定 。
  • 即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。
  • JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為儲存物件,所以無論什麼時候,總是有一塊 Survivor 區域是空閒著的。
  • 新生代實際可用的記憶體空間為 9/10 ( 即90% )的新生代空間。

3.堆的垃圾回收方式

java堆是GC垃圾回收的主要區域。 GC分為兩種: Minor GC、Full GC(也叫做Major GC).

1. Minor GC(簡稱GC)

Minor GC是發生在新生代中的垃圾收集動作, 所採用的是複製演算法。
GC一般為堆空間某個區發生了垃圾回收,
新生代(Young)幾乎是所有java物件出生的地方。即java物件申請的記憶體以及存放都是在這個地方。java中的大部分物件通常不會長久的存活, 具有朝生夕死的特點。
當一個物件被判定為“死亡”的時候, GC就有責任來回收掉這部分物件的記憶體空間。
新生代是收集垃圾的頻繁區域。

回收過程如下:

當物件在 Eden ( 包括一個 Survivor 區域,這裡假設是 from 區域 ) 出生後,在經過一次 Minor GC 後,如果物件還存活,並且能夠被另外一塊 Survivor 區域所容納(上面已經假設為 from 區域,這裡應為 to 區域,即 to 區域有足夠的記憶體空間來儲存 Eden 和 from 區域中存活的物件 ),則使用複製演算法將這些仍然還存活的物件複製到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),並且將這些物件的年齡設定為1,以後物件在 Survivor 區每熬過一次 Minor GC,就將物件的年齡 + 1,當物件的年齡達到某個值時 ( 預設是 15 歲,可以通過引數 -XX:MaxTenuringThreshold 來設定 ),這些物件就會成為老年代。
但這也不是一定的,對於一些較大的物件 ( 即需要分配一塊較大的連續記憶體空間 ) 則是直接進入到老年代。

2.Full GC

Full GC 基本都是整個堆空間及持久代發生了垃圾回收,所採用的是標記-清除演算法。
現實的生活中,老年代的人通常會比新生代的人 “早死”。堆記憶體中的老年代(Old)不同於這個,老年代裡面的物件幾乎個個都是在 Survivor 區域中熬過來的,它們是不會那麼容易就 “死掉” 了的。因此,Full GC 發生的次數不會有 Minor GC 那麼頻繁,並且做一次 Full GC 要比進行一次 Minor GC 的時間更長,一般是Minor GC的 10倍以上。
另外,標記-清除演算法收集垃圾的時候會產生許多的記憶體碎片 ( 即不連續的記憶體空間 ),此後需要為較大的物件分配記憶體空間時,若無法找到足夠的連續的記憶體空間,就會提前觸發一次 GC 的收集動作

擴充套件: Minor GC是如何觸發的, 又是如何工作的? 如下圖:

Minor GC是由位元組碼執行引擎觸發的. 當我們的程式中需要new一個物件的時候, 就會將這個物件放入到Eden區域, 當Eden區域中的物件越來越多, 直到滿了, 這時放不下了, 就會觸發位元組碼執行引擎發起GC操作. 第一次發起的GC, 將會看看哪些物件還活著, 哪些物件已經不用了, 活著的物件放入survivor中的一個區, 不再被引用的物件, 直接被回收了

如何判斷物件是否還活著呢?

位元組碼執行引擎會去找很多gc root.

什麼是gc root呢?

GC Root是一個物件, 以這個物件作為啟動點,從這些節點開始向下搜尋引用的物件, 找到的物件都標記為非垃圾物件, 其餘未標記的物件都是垃圾物件.

GC Root根節點有哪些?

執行緒棧的區域性變數, 方法區中的靜態變數, 本地方法棧的變數等等。

垃圾收集的原理

在Math中, 我們看棧中main方法的區域性變量表中的math變數. 方法區中的user變數. 他們都是GC Root根物件. 他們指向的是一塊堆記憶體空間.
實質是, GC垃圾回收的過程, 就是尋找GC Root的過程. 從棧中找區域性變數, 從方法區中找靜態變數. 從GC Root出發, 找到所有的引用變數. 這些變數可能會引用其他的變數, 變數還會再引用其他變數. 直到不再引用其他變數為止, 以上這些都是非垃圾物件. 如果一個物件沒有被任何物件引用, 那它就是垃圾物件。

垃圾物件最後就被回收, 非垃圾物件進入到Survivor的一個區域裡面. 每次進入sruvivor區域,物件的分代年齡都會+1, 分代年齡儲存在哪裡呢?儲存在物件頭裡面.

程式還在繼續執行, 又會產生新的物件放入到Eden區, 當Eden區又被放滿了, 就會再次出發GC, 此時會尋找Eden+sruvivor(一個區域)中的GC Root, 將其標記,
沒有被引用的物件被回收, 其他被引用的物件會儲存到另一個survivor區域. 分代年齡+1

這樣執行, 直到分代年齡為15(預設15,可設定)時, 也就是GC發生了15次還活著的物件, 就會被放到老年代.

通常什麼樣的物件會被放到老年代呢?

靜態變數引用的物件, 靜態常量. 比如說: 物件池, 快取物件, spring容器裡面的物件,

二. 使用工具檢視GC流轉的過程

我們使用的工具是jvisualvm工具, 這是jdk自帶的一個工具。這個工具通常是在開發環境使用,因為其本身比較耗效能,所以線上一般不用。本地除錯可以使用。

先來準備一段程式碼, 一段很簡單的程式碼, 不停的去產生新的物件

package com.lxl.jvm;

import java.util.ArrayList;
import java.util.List;

public class HeapTest {

    public static void main(String[] args) throws InterruptedException {
        List<User> userList = new ArrayList<>();

        while (true) {
            userList.add(new User());
            Thread.sleep(10);
        }
    }
}

我們來按照上面的邏輯分析程式碼

  1. userList: 是放在棧中的區域性變量表中的一個變數
    new ArrayList<>(): 是放在堆中的一個物件

  2. new User(): 在堆中構建一個新的User物件, 並分配了一個地址,並將這個地址新增到new ArrayList()中.

這裡面 userList是根物件, new User()最終會被newArrayList()引用, 而userList又引用new ArrayList(); 所以, 他們都不會是垃圾, 因此都不會被回收.

那麼死迴圈不停的構造物件, 新增引用. Eden區遲早會放滿, 放滿了就會觸發GC, 那麼GC能把他們回收呢? 回收不了, 因為都在被GC Root直接或間接引用. 最終都會被放入老年代. 然後還在持續構造新的物件,最終會怎麼樣?最終會記憶體溢位. 我們來看看視覺化效果。

首先, 我們啟動程式, 然後在控制檯啟動jvisualvm

我們來看的是HeapTest, 這裡面有很多效能指標可以檢視. 我們重點看visual GC. 如果沒有visual GC 可以參考這篇文章: https://xiaojin21cen.blog.csdn.net/article/details/106612383

從這個圖上,我們可以看到每過一段您時間, 觸發一次GC, 因為不能被回收, 因此會轉移到另一個survivor區域. 經過15次回收, 還沒有收走, 那麼就進入到old老年區.

老年區的物件越來越多, 當老年代物件滿了以後, 會觸發full GC, full GC回收的是整個堆以及方法區的內容. 實際上老年代沒有能夠回收的物件, 這時候在往老年代放, 就會發生OOM

使用這個工具還可以分析我們自己的程式程式碼的垃圾回收清空

三. Stop The World

在發生GC的時候, 會發生STW, Stop the world.

1. 什麼是Stop The World呢 ?

舉個例子:在一個電商網站,使用者正在下單,這是由於記憶體滿了,觸發GC,這時候整個執行緒就會處於停滯狀態。使用者的感受就是一直在loading。。。。直到GC完畢,應用執行緒恢復工作。所以,Stop The World對我們的使用者是有一定影響的。JVM調優主要的目的就是減少Full GC的次數和時間。minor GC也會stop the world,但是他的時間很短,所以我們重點調優還是在full gc

2. 那麼為什麼一定要stop the world呢? 不STW不可以呢?

回答這個問題, 我們可以使用假設法, 假設沒有stop the world 會怎麼樣?

我們知道, 在垃圾回收之前, 要先找到GC Root, 然後標記是否被引用, 最終沒有被引用的物件就是我們要回收的垃圾. 那就是沒有物件引用他了.通常會回收這塊記憶體空間地址 這個時候, 如果主執行緒也在執行, 剛好有一個變數存放在這個記憶體地址了, 而你並行的觸發了GC, 這時候程式就發生混亂了.

這是一種情況,另一種是在觸發GC的過程中,一部分變數正在被標記,而GC已經開始了,標記完以後,發現了垃圾,結果由於GC已經掃描完這裡了,到這這一塊垃圾沒有被清理掉,要等待下一次垃圾回收來清理。