JVM垃圾回收算法的概念與分析
前言
在JVM內(nèi)存模型中會將堆內(nèi)存劃分新生代、老年代兩個區(qū)域,兩塊區(qū)域的主要區(qū)別在于新生代存放存活時間較短的對象,老年代存放存活時間較久的對象,除了存活時間不同外,還有垃圾回收策略的不同,在JVM中中有以下回收算法:
- 標(biāo)記清除
- 標(biāo)記整理
- 復(fù)制算法
- 分代收集算法
有了垃圾回收算法,那JVM是如果確定對象是垃圾對象的呢?判斷對象是否存活JVM也會有幾套自己判斷算法了:
- 引用記數(shù)
- 可達(dá)性分析
有了垃圾回收和判斷對象存在這兩個概念后,再來逐步分析它們。
JVM是如何判斷對象是否存活的?
要是讓開發(fā)人員來判斷一個對象是否有用是很簡單的,簡單的說就是:對象沒有任何引用就認(rèn)為該對象可以被回收了。假設(shè)有如下程序代碼:
public class App { public static void main(){ checkFile("/"); } public static boolean checkFile(String path ){ File file = new File(path); return file.exists(); } }
程序執(zhí)行起來在調(diào)用checkFile的時候JVM圖大概像這樣:
到checkFile方法執(zhí)行完成之后,它里面的局部變量file就會隨著棧幀一起被清理,這個時候還存活在JVM堆中的File對象也是無用的了:
要是人為來判斷非常清晰的就發(fā)現(xiàn)File對象已經(jīng)無用了,那換成JVM它又是如何來判斷對象是否能存活的呢?
引用記數(shù)
引用記數(shù)算法原理比較簡單,想象下有個對象它有一個count屬性,每次引用該對象都會使count加1,假設(shè)JVM在判斷該對象是否存活的時候去檢查這個count屬性,發(fā)現(xiàn)這個屬性不為0說明還有其他對象在引用該對象。
等到checkFile方法執(zhí)行完之后count就會減1變成0:
這樣一來JVM就很容易判斷一個對象是否存活了。
但是引用記數(shù)有一個明顯的缺點,就是無法解決循環(huán)引用的問題比如:A --> B --> A 這樣的對象關(guān)系它是沒有辦法來判斷對象是否該不該回收的。
GC Root(可達(dá)性分析)
為什么會被稱為可達(dá)性分析算法呢?可以這樣理解如果通過GC Root能到達(dá)一個對象那么這個對象就是存活的。那什么樣的對象才是GC Root呢?
在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機(jī)棧中引用的對象(棧幀中的本地變量表);
- 方法區(qū)中類靜態(tài)屬性引用的對象;
- 方法區(qū)中常量引用的對象;
- 本地方法棧中JNI(Native方法)引用的對象。
還是用上面的例子,在checkFile方法執(zhí)行時,因為棧幀變量file可做為GC Root所以在執(zhí)行期間JVM是絕對不會回收掉這個File對象:
但是等到checkFile執(zhí)行完成之后,這個棧幀會被彈出,其中的變量也會被釋放,相應(yīng)的沒有GC Root能到達(dá)堆中的File對象,這個時候就可以判斷這個對象是一個無用的對象了,然后安全回收。
垃圾收回算法
標(biāo)記清除
這種算法分兩分:標(biāo)記、清除兩個階段,
標(biāo)記階段是從根集合(GC Root)開始掃描,每到達(dá)一個對象就會標(biāo)記該對象為存活狀態(tài),清除階段在掃描完成之后將沒有標(biāo)記的對象給清除掉。
用一張圖說明:
這個算法有個缺陷就是會產(chǎn)生內(nèi)存碎片,如上圖B被清除掉后會留下一塊內(nèi)存區(qū)域,如果后面需要分配大的對象就會導(dǎo)致沒有連續(xù)的內(nèi)存可供使用。
標(biāo)記整理
標(biāo)記整理就沒有內(nèi)存碎片的問題了,也是從根集合(GC Root)開始掃描進(jìn)行標(biāo)記然后清除無用的對象,清除完成后它會整理內(nèi)存。
這樣內(nèi)存就是連續(xù)的了,但是產(chǎn)生的另外一個問題是:每次都得移動對象,因此成本很高。
復(fù)制算法
復(fù)制算法會將JVM推分成二等分,如果堆設(shè)置的是1g,那使用復(fù)制算法的時候堆就會有被劃分為兩塊區(qū)域各512m。給對象分配內(nèi)存的時候總是使用其中的一塊來分配,分配滿了以后,GC就會進(jìn)行標(biāo)記,然后將存活的對象移動到另外一塊空白的區(qū)域,然后清除掉所有沒有存活的對象,這樣重復(fù)的處理,始終就會有一塊空白的區(qū)域沒有被合理的利用到。
兩塊區(qū)域交替使用,最大問題就是會導(dǎo)致空間的浪費,現(xiàn)在堆內(nèi)存的使用率只有50%。
分代回收
新生代回收
JVM的堆分為新生代和老年代,兩種類型有不同的特性,根據(jù)它們的特性來選擇不同的回收算法,這種算法會將新生代劃分為一塊Eden和二個Survivor區(qū):
如上面的圖有三塊區(qū)域它們會按照8:1:1的比例進(jìn)行分配,如1000m的堆Eden是800m,二個Survivor各占100m,那它們是如何運(yùn)行的呢?
- 始終會有一塊Survivor是空著的,內(nèi)存使用率是90%
- 程序運(yùn)行會在Eden和其中一塊Survivor 1中分配內(nèi)存
- 等到執(zhí)行Minor gc,會將存活下來的對象移動到空著的Survivor 2中
- 然后在Eden和Survivor 2中繼續(xù)分配內(nèi)存,Survivor 1空著等著下次使用
這樣就能使內(nèi)存使用率達(dá)到90%,也不會產(chǎn)生內(nèi)存碎片。
老年代回收
老年代對象即使進(jìn)行了垃圾回收,對象的存活率也高,所以采用標(biāo)記清除或標(biāo)記整理算法都是不錯的選擇,這里就不做闡述。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對腳本之家的支持。
相關(guān)文章
Java 下數(shù)據(jù)業(yè)務(wù)邏輯開發(fā)技術(shù) JOOQ 和 SPL
這篇文章主要為大家介紹了Java 下數(shù)據(jù)業(yè)務(wù)邏輯開發(fā)技術(shù) JOOQ 和 SPL詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09Java數(shù)據(jù)結(jié)構(gòu)之List的使用總結(jié)
List是Java中比較常用的集合類,指一系列存儲數(shù)據(jù)的接口和類,可以解決復(fù)雜的數(shù)據(jù)存儲問題,本文就來拿實際案例總結(jié)介紹一下List的使用方法,感興趣的朋友快來看看吧2021-11-11SpringBoot集成Flink-CDC實現(xiàn)對數(shù)據(jù)庫數(shù)據(jù)的監(jiān)聽問題
Flink CDC(Flink Change Data Capture)是一種基于數(shù)據(jù)庫日志的CDC技術(shù),它實現(xiàn)了一個全增量一體化的數(shù)據(jù)集成框架,這篇文章主要介紹了SpringBoot集成Flink-CDC,實現(xiàn)對數(shù)據(jù)庫數(shù)據(jù)的監(jiān)聽,需要的朋友可以參考下2024-07-07Java并發(fā)編程(CyclicBarrier)實例詳解
這篇文章主要介紹了Java并發(fā)編程(CyclicBarrier)實例詳解的相關(guān)資料,JAVA編寫并發(fā)程序的時候,我們需要仔細(xì)去思考一下并發(fā)流程的控制,如何讓各個線程之間協(xié)作完成某項工作。2017-07-07