首頁>技術>

背景

在Java中有一個很重要的概念,即一切皆物件。所謂物件,就是將現實中的事物抽象出來,進而可以透過繼承、實現和組合的方式把萬事萬物都給容納,所以理解物件的概念在學習Java(包括所有的面向物件的語言)的過程中至關重要。

當我們在程式中需要使用某個物件的時候,它就是爺爺,即使採用反射的方法也得把它創建出來;當我們不需要它的時候,它就是個垃圾,即使它能逃過新生代,在老年代也要依然追殺你。

今天,我們要學習的是JVM如何處理垃圾(物件),在學習之前,我們先思考以下幾個問題:

物件的生存週期有哪些階段?如何判斷一個物件已經成為了垃圾?垃圾如何標記?如何回收垃圾(物件),有哪些策略?1、物件的生存週期

通常情況下,作為一名Java開發者,我們可以肆無忌憚的new物件,而不需要管理這些物件的生存週期,因為JVM會幫我們打掃衛生(管理物件)。但是,作為進階的Coder,我們還是要認真理解一下的。下面我們先了解下的物件的生存週期。

1.1宏觀視角

從宏觀的角度看,物件的生存週期可以是:物件建立--->物件的使用--->物件的回收。

物件建立

物件建立可以採用new指令、反序列化、反射等方法,這一步驟主要是為物件分配記憶體並初始化。

物件的使用

物件的使用是將JVM棧中的物件的引用去定位、訪問堆中的物件的具體位置,常採用的方法有控制代碼和直接指標。

物件的回收

物件的回收就是垃圾回收,我們在下文中詳解

僅僅看這三大塊還是有點不明所以,太粗糙,下面我們以微觀的角度分析物件的生存週期。

1.2微觀視角

從微觀視角來看,物件的生存的週期可以大致分為七個階段:建立階段、應用階段、不可見階段、不可達階段、可收集階段、終結階段、物件空間重分配階段。

建立階段

物件建立階段主要是為物件分配記憶體空間、開始構造物件並完成對static成員的初始化。物件一旦被建立,並且分派給某些變數賦值,這個物件的狀態就被切換到了應用階段。

應用階段

應用階段就是物件至少被一個強引用關聯著。(莫慌,強引用的概念在下文中講解)

不可見階段

當一個物件處於不可見階段時,說明程式不再持有該物件的任何強引用,但是這些引用可能還存在著,一般情況下是指程式的執行已經超過該作用域了。

boolean flag= false;if(flag){  flag = 0;  num++;        }System.out.println(num);

在上面的程式中,本地變數num在System.out.println(num)時已經超出了其作用域,程式就認為變數num處於不可見階段。

不可達階段

物件處於不可達階段是指該物件不再被任何強引用所持有,但是該物件仍可能被JVM等系統下的某些已裝載的靜態變數或執行緒或JNI等強引用持有著,這些特殊的強引用被稱為”GC root”。存在著這些GC root會導致物件的記憶體洩露情況,無法被回收.

可收集階段

當垃圾回收器發現該物件已經處於“不可達階段”而且垃圾回收器已經對該物件的記憶體空間又一次分配做好準備時,則物件進入了“收集階段”。

終結階段

當物件執行完finalize方法後仍然處於不可達狀態時,則該物件進入終結階段。在該階段是等待垃圾回收器對該物件空間進行回收。

物件空間重分配階段

物件空間又一次分配階段,垃圾回收器對該物件的所佔用的記憶體空間進行回收或者再分配了,則該物件徹底消失了,稱之為“物件空間又一次分配階段”。

上面落落索索一大堆,看得小夥伴似懂非懂,而小夥伴只想知道什麼時候物件變垃圾以及垃圾(物件)被清除的。

事實上,這些階段伴隨著物件的建立、物件的使用、物件失效、物件被標記為垃圾、物件被回收的整個流程。為了滿足你們的要求,我們把問題聚焦到垃圾(物件)的問題上,不過,在聚焦問題之前,我們最先理解下物件引用的概念,因為這對於物件變垃圾非常有幫助!

1.3物件引用

從JDK1.2開始,Java的設計人員將物件的引用分為四類,分別是:強引用、軟引用、弱引用和虛引用。

強引用

強引用表示一個物件處在【有用,必須】的狀態,它是使用最普遍的引用。如果一個物件具有強引用,那麼垃圾回收器絕不會回收它。就算在記憶體空間不足的情況下,Java虛擬機器寧可丟擲OutOfMemoryError錯誤,使程式異常終止,也不會透過回收具有強引用的物件來解決記憶體不足的問題。

Student student = new Student(); // 這就是強引用
軟引用

軟引用表示一個物件處在【有用,但非必須】的狀態。在記憶體空間足夠的情況下,如果一個物件只具有軟引用,那麼垃圾回收器就不會回收它,但是如果記憶體空間不足,垃圾回收器就會回收這個物件(回收發生在OutOfMemoryError錯誤之前)。只要垃圾回收器沒有回收它,這個物件就能被程式使用。

軟引用用來實現記憶體敏感的快取記憶體,比如說:網頁快取、圖片快取等。

Student student = new Student();SoftReference softReference = new SoftReference(student);
弱引用

弱引用表示一個物件處在【可能有用,但非必須】的狀態。類似於軟引用,但是強度比軟引用更弱一些:只具有弱引用的物件擁有更短暫的生命週期。GC執行緒在掃描它所管轄的記憶體區域的過程中,一旦發現只具有弱引用的物件,就會回收掉這些被弱引用關聯的物件。也就是說,無論當前記憶體是否緊缺,GC都會回收被弱引用關聯的物件。不過,由於GC是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。

經典實用案列java.langThreadLocal中

虛引用

虛引用表示一個物件處在【無用】的狀態,意味著虛引用等同於沒有引用,在任何時候都可能被GC回收。設定虛引用的目的是為了被虛引用關聯的物件在被垃圾回收器回收的時候,能夠收到一個系統通知(用來跟蹤物件被GC回收的活動)。

ReferenceQueue referenceQueue = new ReferenceQueue();PhantomReference phantomReference = new PhantomReference(object,queue);
2、垃圾(物件)判斷

垃圾回收的第一步就是要判斷物件是否是垃圾。事實上,一個物件是否是垃圾不是由我們程式設計師來判斷的,更準確的說是我們Java Developer不需要的關心的,如果你真的是有程式碼潔癖或者強迫症,你呼叫gc方法也無可厚非。

事實上,垃圾回收主要有兩種演算法:引用計數演算法和可達性分析演算法

2.1引用計數演算法

引用計數演算法是一種很古老的演算法,現在在很多java版本已經廢棄掉了,作為一名學習者,還有是有必要了解一下的。

定義:引用計數演算法是在物件中添加了一個引用計數器,當引用這個物件時,計數就加1;當引用失效的時候,計數器就減1,當引用計數數為0的時候,該物件也就失效了,變成了垃圾,JVM也就開始回收它了。

從定義上來看,引用計數演算法還是很容易理解的,它是一個優缺點很明顯的一種演算法,我們接著往下看。

優點

①引用計數演算法原理簡單,並且實時性較強,當引用計數器為0的時候,JVM就可以直接對它進行回收。

②引用計數器只作用於單個物件,即JVM掃描時,只會掃描該物件,不會順著引用掃描全部物件。

缺點

①物件每次被引用時,都需要耗費一定的時間去更新引用計數器。

②會出現引用迴圈問題,即物件A引用物件B,物件B引用物件A,由於A和B相互引用,計數它們不被需要了,它們也不會被JVM當做垃圾回收。

為了解決引用計數演算法產生的問題,優秀的Java開發者提出了另一種演算法:可達性分析演算法。

2.2可達性分析演算法

可達性分析演算法的思路是透過一系列的“GC Roots”物件作為起點進行搜尋,如果在“GC Roots”和一個物件之間沒有可達路徑,則稱該物件是不可達的,則認為該物件可以被回收。

可達性分析演算法如下圖所示:

與引用計數演算法的不同的是,引用計數演算法判斷的是物件是否死亡,而可達性分析演算法分析的物件是否還活著,可達性分析演算法可以有效解決引用計數演算法中的迴圈引用問題。可達性分析演算法是判斷物件是否為垃圾的主流演算法。

3、垃圾標記

上文中我們已經講解了如何判斷一個物件是否是垃圾,及採用的是引用計數法和可達性分析法。小夥伴肯定發現這兩種演算法是即判斷物件是否為垃圾,同時標記了物件。

是的,更準確的說:如果一個物件不存在引用,那麼這個物件就是垃圾。而引用計數演算法中的計數器和可達性分析演算法中的引用鏈都是標記物件的方式。

我們也在上面說過,由於引用計數演算法存在迴圈引用問題,所以現有的主流標記演算法是可達性分析演算法。下面,我們將詳細分析下可達性分析演算法標記垃圾物件的過程。

在Java中,可以作為GC Roots的物件通常包含以下幾種:

虛擬機器棧中引用的物件方法區中靜態屬性引用的物件方法區中常量引用的物件本地方法中(Native)引用的物件

在可達性分析演算法中,即使存在不可達的物件,該物件也不一定是非死不可的,一個物件要真正的死亡,要經歷兩次標記的過程。

標記過程分析:

當使用可達性分析演算法分析物件時,若發現一些物件與GC Root鏈不可達,那麼該物件就會被第一次標記,然後進行篩選,篩選的條件是判斷該物件有沒有必要執行finalize()方法(此方法每個物件預設都有),但如果物件沒有重寫finalize()方法或者物件的finalize方法已經被虛擬機器呼叫過一次了,則都將視為“沒有必要執行”,垃圾回收器可以直接回收。

如果該物件被判定為有必要執行finalize()方法,那麼虛擬機器會把這個物件放置在一個的佇列中,然後由一個專門的Finalizer執行緒去執行這個物件的finalize()方法。如果此時存在某些物件重新與引用鏈上的任何一個物件建立了關聯,那麼在第二次標記時它將被移這個佇列。

4、垃圾回收

記憶體是很寶貴的資源,對於不需要的垃圾物件,我們要儘管把它驅逐出去。上文講解的垃圾(物件)判斷、垃圾標記步驟都是為垃圾回收做鋪墊。

在文章JVM記憶體中,我們詳細分析JVM的執行時資料區,不熟悉的同學可以再去學習下。

4.1堆的佈局結構分析

事實上,垃圾回收的地方是執行時資料區的堆中,我們都知道物件的存活是有周期的,如果一個物件沒有被引用,那麼就可以認為該物件可以被清除掉了,就是我們認為的垃圾。由於每個物件存活的時間不同,為了減少GC執行緒掃描垃圾時間及頻率,我們可以將存活時間較長的物件單獨放一個區域。因此,堆的佈局也就確定下來了。總的來說,堆被劃分成兩部分:新生代和老年代。

image-20210205113109427

新生代和老年代比值為1:2,這個比例並不是唯一的,我們可以可以透過引數 –XX:NewRatio按照具體的場景來指定,如果再細粒度的劃分,新生代又可以分為Eden區和Survivor區,而Survivor區又可以分為FromSurvivor和ToSurvivor,預設比值為8:1:1

這時問題又來了,為什麼要將Survivor分為兩塊相等大小的空間啊?好問題,我先說答案,這兩分為兩部分主要是為了解決記憶體碎片化的問題,如果記憶體碎片化嚴重,也就是兩個物件佔用不連續的記憶體,已有的連續記憶體不夠新物件存放,就會觸發垃圾回收(GC)。

4.2垃圾回收演算法

上文中我們分析了執行時資料區中的堆的佈局結構,按照物件的生存週期將堆分成了新生代、老年代,新生代又細分為了三個區:Eden,From Survivor,To Surviver,比例是8:1:1。理解堆的佈局結構對理解JVM中的垃圾回收演算法的流程非常有幫助。為什麼這麼說呢?因為垃圾物件主要是在堆中,又因為堆切分了不同的分割槽,根據每塊分割槽特性採用的垃圾回收演算法也是不同的。

在下面的學習中,我們先學習下常用的垃圾回收演算法,最後根據堆中不同的分割槽記憶體的特性選擇合適的垃圾回收演算法。

通常情況下,常用的垃圾回收演算法有標記-清除演算法、標記複製演算法、標記整理演算法和分代回收演算法。

標記-清除演算法

標記清除演算法分為標記和清除兩個階段,即首先標記出需要回收的物件,標記完成後統一清理物件。它的優點是效率高,缺點是容易產生記憶體碎片。

標記階段是以根節點(GC Roots)為起點在引用鏈上進行掃描,對存活的物件進行標記。清除階段是掃描整個物件集合,清除集合中未被標記的物件。

垃圾標記和垃圾回收階段如下圖所示:

標記-複製演算法

標記複製演算法是將記憶體劃分兩個空間,在任意的時間點,所有動態分配的物件都只能分配在其中一個空間,這個空間可以被稱為活動空間,而另外一個空間則是空閒的。當有效記憶體空間耗盡時,JVM將暫停程式執行,開啟複製演算法GC執行緒。接下來GC執行緒會將活動區間內的存活物件,全部複製到空閒區間,且嚴格按照記憶體地址依次排列,與此同時,GC執行緒將更新存活物件的記憶體引用地址指向新的記憶體地址。

此時,空閒區間已經與活動區間交換,而垃圾物件現在已經全部留在了原來的活動區間,也就是現在的空閒區間。事實上,在活動區間轉換為空間區間的同時,垃圾物件已經被一次性全部回收。

標記整理演算法

標記整理演算法與標記清除演算法很相似,也是分為兩個階段:標記和整理,。

標記:它的第一個階段與標記/清除演算法是一模一樣的,均是遍歷GC Roots,然後將存活的物件標記。

整理:移動所有存活的物件,且按照記憶體地址次序依次排列,然後將末端記憶體地址以後的記憶體全部回收。因此,第二階段才稱為整理階段

分代回收演算法

目前大多數JVM垃圾收集器採用的演算法都是分代回收演算法,其思想是根據物件存活的生命週期將記憶體劃分成三個區域:新生代、老年代和永久代。劃分區域可以參考堆的記憶體結構,新生代和老年代和堆中的劃分是一一對應,而永久代在Java8中已經用元空間代替了。

新生代回收演算法

新生代主要存放生命週期較短的物件,所有新生成的物件首先都應該放在新生代中的Eden區,回收時將Eden區存活物件複製到To Survivor區,然後清空eden區。當To Survivor區也存放滿了時,則將Eden區和To Survivor區存活物件複製到From Survivor區,然後清空Eden和這個From Survivor區,此時To Survivor區為空,然後將To Survivor區和From Survivor區交換,即保持From Survivor區為空, 如此往復。

當From Survivor區不足以存放 Eden區和To Survivor區的存活物件時,就將存活物件直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收。

一般情況下,新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)。

老年代回收演算法

在新生代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。老年代的記憶體比新生代也大很多(大概比例是1:2),當老年代記憶體滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代物件存活時間比較長,存活率標記高。

永久代回收演算法

永久代用於存放靜態檔案,如Java類、方法等。永久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者呼叫一些class,例如Hibernate 等,在這種時候需要設定一個比較大的永久代空間來存放這些執行過程中新增的類。在JDK1.8之前,永久代稱方法區,在JDK1.8後,永久代被稱為元空間。

堆結構中的不同分割槽中的演算法選擇

事實上,JVM對堆中的不同區域(新生代和老年代)採用不同的演算法。

新生代比較適合複製演算法,新生代有Eden、From Survivor和To Survivor三個區,因為Eden區中物件會被複制到To Survivor,且From Survivor和To Survivor相互交換比較頻繁,所以採用複製演算法。

老年代中的物件的生命週期比較長,所以不適合複製演算法,在老年代一般採用的是標記-整理/清除演算法。

原文連結:https://xie.infoq.cn/article/0494fd7554ecf2788344f3b1d

8
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 記憶體資料庫在金融領域高效能、低延時場景下的應用