1. 程式人生 > 實用技巧 >真的可惜,四面阿里,結果我被JVM垃圾回收機制與 OOM異常卡住了

真的可惜,四面阿里,結果我被JVM垃圾回收機制與 OOM異常卡住了

前言

為什麼需要垃圾回收

  • 首先我們來聊聊為什麼會需要垃圾回收,假設我們不進行垃圾回收會造成什麼後果,我們舉一個簡單的例子
  • 我們住在一個房子裡面,我們每天都在裡面生活,然後垃圾都丟在房子裡面,又不打掃,最後房子都是垃圾 我們是不是就沒法住下去了。
  • 所以JVM垃圾回收機制也是一樣的,當我們建立的物件佔據堆空間要滿了的的時候我們就對他進行垃圾回收,注意java的垃圾回收是不定時的,c語言的是需要去呼叫垃圾回收方法
  • 剛剛也說到 上面舉的例子也說到 假設一個房子都被垃圾堆滿了 那麼我們沒法住人了 那麼我們是不是會告訴別人這個房子沒法住人了 而java也是如此當我們堆空間滿了的時候 此時它就會丟擲異常OutOfMemoryError(簡稱OOM)

什麼地方需要進行垃圾回收

剛剛我們說了為什麼要回收垃圾,和什麼是OOM那麼我們下面就給大家介紹,我們JVM中什麼地方需要進行垃圾回收。

垃圾回收要考慮的點

1)是否會產生垃圾
2)哪些記憶體需要回收
3)什麼時候回收
4)如何對他進行回收

程式計數器
jvm中唯一 一個不需要垃圾回收的地方。
棧 本地方法棧
這個地方會因為棧幀存滿了導致記憶體溢位,所以需要垃圾回收
方法區(元空間)
這個地方也需要進行垃圾回收

這個地方是我們垃圾回收最頻繁的地方,我們幾乎我們所有的物件都儲存在堆中,也是我們今天要著重講的地方

堆GC

堆,可能大家都不陌生,可是好像又距離我們很遠,今天它來了

從上面的圖我們可以看出 ,我們的堆空間被主要被劃分為了二塊區域 ,新生代 ,老年代 java堆是我們JVM中管理區域最大的一塊, java堆是一個執行緒共享的區域 ,在虛擬機器啟動時建立 ,幾乎所有的物件都在此分配, java虛擬機器規範中有過描述,所有的例項物件以及陣列都在堆中進行分配記憶體, 但目前因為JTI編譯器的發展,和逃逸分析技術的逐漸完善,在堆中分配物件也不是那麼的絕對了。
堆的記憶體在物理上可以是不連續的,但是在邏輯上是即可。

新生代

  • 我們物件的建立到結束,幾乎是"朝生夕死"的一個過程差不多90%的物件都在新生代被回收了,所以新生代的gc也是發生最為頻繁的一個區域。新生代產生gc我們稱為Y-GC
  • 每產生一次y-gc我們物件的年齡就加一歲,直到15歲後進入老年代。當然這是正常情況,那麼有沒有特殊情況勒當然有

空間擔保

  • 當我們建立的物件大於Eden的時候,此時怎麼辦,此時他會先產生一次Y-GC如果還是無法儲存下新建立的物件,那麼我們就會通過空間擔保策略進入老年代。
  • 還有一種情況,物件建立也會直接進入老年代,當我們的Surivivor區滿了的時候,此時它不會主動產生gc只會依賴於Eden,但我們的物件又不能被拋棄,所以它也被分配到了老年代
  • 當然我們實際開發工作中需要儘量的去避免這種情況的誕生

動態年齡

  • 什麼是動態年齡勒,這是堆中的另一個,擔保策略了,它會去判斷我們Surivivor的區中,相同年齡的物件大於Surivivor區一半的時候,那麼他就會判定此時這些物件已經能夠很好的存活了,所以他們就集體被丟到老年代了

物件如何分配記憶體

老年代

  • 我們老年代存放的都是一些老物件了,大物件,都是存活時間較長的物件這裡一般很少產生FGC這裡一旦產生FGC那麼所產生GC的耗時將會是YGC的10倍時耗,而我們老年代快要存滿時進入了一個物件,這時會產生一次FGC如果GC結束後,還是無法存放物件的話此時就會報OOM異常。

垃圾回收演算法

分代演算法

分代演算法,其實也就是將我們堆空間劃分為了一個個不同的區域,新生代,老年代,不讓它回收的時候對整個堆進行一個回收。減少GC所停頓的時間,我們稱之為STW (Stop The World),假設我們堆整個堆進行垃圾回收,是不是每次都需要去把整個堆的垃圾標記一次,非常的那麼使用者執行緒停止的時間就非常長,你想一下,假如你的電腦每使用1個小時就卡10秒,那麼你是不是非常操蛋。

標記清除

演算法執行過程

堆空間垃圾清理前

垃圾清除後

演算法介紹
標記清除是最開始jvm選擇的一種垃圾回收演算法,這個演算法就和他的名字一樣,分為標記,和清除二個過程,首先他會標記所有需要回收的物件,標記結束後對標記的物件進行一個垃圾回收。
缺點
這種方式會有什麼缺點呢!它會導致記憶體空間的浪費,產生大佬不連續的記憶體碎片,當我們需要一個連續的記憶體空間存放大物件的時候,因為連續的記憶體空間不夠,導致我們不得又產生一次GC,提高了我們GC產生的頻率。

標記整理

演算法執行過程

演算法介紹
標記整理,的執行過程,於標記清除相反,標記清除是標記需要回收的物件,而標記整理卻是標記存活的物件,然後把他們全部向一段進行位移,然後清除端邊界以外的所有物件。
適用範圍
老年代垃圾回收

複製演算法

演算法執行過程

演算法介紹
複製演算法也是在標記清除上的一個改進,它彌補了標記清除出現大量不連續記憶體碎片的缺點。它將一個可用的記憶體空間劃分為大小相等的二塊區域,每次只使用其中一塊區域,當這塊區域用完了就把存活的物件放到,另一塊空著的區域區,然後把自己清除乾淨,變成一塊空著的區域。這樣就解決了記憶體碎片的問題
缺點
那麼這樣的演算法是不是太過於苛刻了,每次都需要一塊空著的區域用於存放物件,犧牲掉了大量的記憶體。
適用範圍
新生代Surivivor區域

堆中物件記憶體的分配策略

指標碰撞

這種分配方式其實是複製演算法,標記整理中的攜帶的一種物件分配策略,我們如何區分什麼是用過的,什麼是沒用過的,這時我們通過一個指標,作為一個分界點指示器,那所需要分配的記憶體,就僅僅是把指示器指標向空閒空間那邊挪動一段與物件大小相等的距離,這種分配方式稱為"指標碰撞"(Bump the Pointer)。

空閒列表

空閒是標記清除中對物件分配的一個策略,因為標記清除中我們的記憶體劃分的隨機的,已使用記憶體和未使用記憶體相互交錯,那麼我們如何把他們關聯起來,虛擬機器針對這種交錯的記憶體維護了一個列表,記錄哪些記憶體塊是可用的,在分配的時候找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式成為"空閒列表"(Free List)。

OOM異常

其實OOM在上面介紹了堆記憶體的劃分和收集過程中,大家也應該對它有了一定的認識了,OOM異常是發生在老年代Old中的一個異常,當我們老年代中無法在存放物件的時候,就會報OOM記憶體溢位異常

public class HeapOomError {
    public static void main(String[] args) {
        List<byte[]> list =new ArrayList<>();
        int i=0;
        while (true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e){
                e.printStackTrace();
            }
            list.add(new byte[5 * 1024 * 1024]);
            //System.out.println("count is:"+(++i));
        }
    }
}

設定堆空間的大小

最後我們得到的結果如下

總結

總而言之我們需要的優化的GC的損耗和避免記憶體溢位的出現,從而提高我使用者良好使用體驗。

最後

感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!