JVM垃圾回收算法的概念與分析
前言
在JVM內(nèi)存模型中會(huì)將堆內(nèi)存劃分新生代、老年代兩個(gè)區(qū)域,兩塊區(qū)域的主要區(qū)別在于新生代存放存活時(shí)間較短的對(duì)象,老年代存放存活時(shí)間較久的對(duì)象,除了存活時(shí)間不同外,還有垃圾回收策略的不同,在JVM中中有以下回收算法:
- 標(biāo)記清除
- 標(biāo)記整理
- 復(fù)制算法
- 分代收集算法
有了垃圾回收算法,那JVM是如果確定對(duì)象是垃圾對(duì)象的呢?判斷對(duì)象是否存活JVM也會(huì)有幾套自己判斷算法了:
- 引用記數(shù)
- 可達(dá)性分析
有了垃圾回收和判斷對(duì)象存在這兩個(gè)概念后,再來(lái)逐步分析它們。
JVM是如何判斷對(duì)象是否存活的?
要是讓開(kāi)發(fā)人員來(lái)判斷一個(gè)對(duì)象是否有用是很簡(jiǎn)單的,簡(jiǎn)單的說(shuō)就是:對(duì)象沒(méi)有任何引用就認(rèn)為該對(duì)象可以被回收了。假設(shè)有如下程序代碼:
public class App {
public static void main(){
checkFile("/");
}
public static boolean checkFile(String path ){
File file = new File(path);
return file.exists();
}
}
程序執(zhí)行起來(lái)在調(diào)用checkFile的時(shí)候JVM圖大概像這樣:

到checkFile方法執(zhí)行完成之后,它里面的局部變量file就會(huì)隨著棧幀一起被清理,這個(gè)時(shí)候還存活在JVM堆中的File對(duì)象也是無(wú)用的了:

要是人為來(lái)判斷非常清晰的就發(fā)現(xiàn)File對(duì)象已經(jīng)無(wú)用了,那換成JVM它又是如何來(lái)判斷對(duì)象是否能存活的呢?
引用記數(shù)
引用記數(shù)算法原理比較簡(jiǎn)單,想象下有個(gè)對(duì)象它有一個(gè)count屬性,每次引用該對(duì)象都會(huì)使count加1,假設(shè)JVM在判斷該對(duì)象是否存活的時(shí)候去檢查這個(gè)count屬性,發(fā)現(xiàn)這個(gè)屬性不為0說(shuō)明還有其他對(duì)象在引用該對(duì)象。

等到checkFile方法執(zhí)行完之后count就會(huì)減1變成0:

這樣一來(lái)JVM就很容易判斷一個(gè)對(duì)象是否存活了。
但是引用記數(shù)有一個(gè)明顯的缺點(diǎn),就是無(wú)法解決循環(huán)引用的問(wèn)題比如:A --> B --> A 這樣的對(duì)象關(guān)系它是沒(méi)有辦法來(lái)判斷對(duì)象是否該不該回收的。
GC Root(可達(dá)性分析)
為什么會(huì)被稱為可達(dá)性分析算法呢?可以這樣理解如果通過(guò)GC Root能到達(dá)一個(gè)對(duì)象那么這個(gè)對(duì)象就是存活的。那什么樣的對(duì)象才是GC Root呢?
在Java語(yǔ)言中,可作為GC Roots的對(duì)象包括下面幾種:
- 虛擬機(jī)棧中引用的對(duì)象(棧幀中的本地變量表);
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象;
- 方法區(qū)中常量引用的對(duì)象;
- 本地方法棧中JNI(Native方法)引用的對(duì)象。
還是用上面的例子,在checkFile方法執(zhí)行時(shí),因?yàn)闂兞縡ile可做為GC Root所以在執(zhí)行期間JVM是絕對(duì)不會(huì)回收掉這個(gè)File對(duì)象:

但是等到checkFile執(zhí)行完成之后,這個(gè)棧幀會(huì)被彈出,其中的變量也會(huì)被釋放,相應(yīng)的沒(méi)有GC Root能到達(dá)堆中的File對(duì)象,這個(gè)時(shí)候就可以判斷這個(gè)對(duì)象是一個(gè)無(wú)用的對(duì)象了,然后安全回收。
垃圾收回算法
標(biāo)記清除
這種算法分兩分:標(biāo)記、清除兩個(gè)階段,
標(biāo)記階段是從根集合(GC Root)開(kāi)始掃描,每到達(dá)一個(gè)對(duì)象就會(huì)標(biāo)記該對(duì)象為存活狀態(tài),清除階段在掃描完成之后將沒(méi)有標(biāo)記的對(duì)象給清除掉。
用一張圖說(shuō)明:

這個(gè)算法有個(gè)缺陷就是會(huì)產(chǎn)生內(nèi)存碎片,如上圖B被清除掉后會(huì)留下一塊內(nèi)存區(qū)域,如果后面需要分配大的對(duì)象就會(huì)導(dǎo)致沒(méi)有連續(xù)的內(nèi)存可供使用。
標(biāo)記整理
標(biāo)記整理就沒(méi)有內(nèi)存碎片的問(wèn)題了,也是從根集合(GC Root)開(kāi)始掃描進(jìn)行標(biāo)記然后清除無(wú)用的對(duì)象,清除完成后它會(huì)整理內(nèi)存。

這樣內(nèi)存就是連續(xù)的了,但是產(chǎn)生的另外一個(gè)問(wèn)題是:每次都得移動(dòng)對(duì)象,因此成本很高。
復(fù)制算法
復(fù)制算法會(huì)將JVM推分成二等分,如果堆設(shè)置的是1g,那使用復(fù)制算法的時(shí)候堆就會(huì)有被劃分為兩塊區(qū)域各512m。給對(duì)象分配內(nèi)存的時(shí)候總是使用其中的一塊來(lái)分配,分配滿了以后,GC就會(huì)進(jìn)行標(biāo)記,然后將存活的對(duì)象移動(dòng)到另外一塊空白的區(qū)域,然后清除掉所有沒(méi)有存活的對(duì)象,這樣重復(fù)的處理,始終就會(huì)有一塊空白的區(qū)域沒(méi)有被合理的利用到。

兩塊區(qū)域交替使用,最大問(wèn)題就是會(huì)導(dǎo)致空間的浪費(fèi),現(xiàn)在堆內(nèi)存的使用率只有50%。
分代回收
新生代回收
JVM的堆分為新生代和老年代,兩種類型有不同的特性,根據(jù)它們的特性來(lái)選擇不同的回收算法,這種算法會(huì)將新生代劃分為一塊Eden和二個(gè)Survivor區(qū):

如上面的圖有三塊區(qū)域它們會(huì)按照8:1:1的比例進(jìn)行分配,如1000m的堆Eden是800m,二個(gè)Survivor各占100m,那它們是如何運(yùn)行的呢?
- 始終會(huì)有一塊Survivor是空著的,內(nèi)存使用率是90%
- 程序運(yùn)行會(huì)在Eden和其中一塊Survivor 1中分配內(nèi)存
- 等到執(zhí)行Minor gc,會(huì)將存活下來(lái)的對(duì)象移動(dòng)到空著的Survivor 2中
- 然后在Eden和Survivor 2中繼續(xù)分配內(nèi)存,Survivor 1空著等著下次使用
這樣就能使內(nèi)存使用率達(dá)到90%,也不會(huì)產(chǎn)生內(nèi)存碎片。
老年代回收
老年代對(duì)象即使進(jìn)行了垃圾回收,對(duì)象的存活率也高,所以采用標(biāo)記清除或標(biāo)記整理算法都是不錯(cuò)的選擇,這里就不做闡述。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
springboot項(xiàng)目數(shù)據(jù)庫(kù)配置類DatabaseConfig示例詳解
這篇文章主要介紹了springboot項(xiàng)目數(shù)據(jù)庫(kù)配置類DatabaseConfig實(shí)現(xiàn)代碼,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08
Spring常用注解及自定義Filter的實(shí)現(xiàn)
這篇文章主要介紹了Spring常用注解及自定義Filter的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
java定位死鎖的三種方法(jstack、Arthas和Jvisualvm)
這篇文章主要給大家介紹了關(guān)于java定位死鎖的三種方法,分別是通過(guò)jstack定位死鎖信息、通過(guò)Arthas工具定位死鎖以及通過(guò) Jvisualvm 定位死鎖,文中還介紹了死鎖的預(yù)防方法,需要的朋友可以參考下2021-09-09
Java容器ArrayList知識(shí)點(diǎn)總結(jié)
本篇文章給大家分享了Java容器ArrayList的相關(guān)知識(shí)點(diǎn),對(duì)此有需要的朋友可以跟著學(xué)習(xí)參考下。2018-05-05
java類Circle定義計(jì)算圓的面積和周長(zhǎng)代碼示例
要用Java計(jì)算圓的周長(zhǎng)和面積,需要使用圓的半徑和一些數(shù)學(xué)公式,下面這篇文章主要給大家介紹了關(guān)于java類Circle定義計(jì)算圓的面積、周長(zhǎng)的相關(guān)資料,需要的朋友可以參考下2024-04-04
解決@SpringBootTest 單元測(cè)試遇到的坑
這篇文章主要介紹了解決@SpringBootTest 單元測(cè)試遇到的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10
解決程序啟動(dòng)報(bào)錯(cuò)org.springframework.context.ApplicationContextExcept
文章描述了一個(gè)Spring Boot項(xiàng)目在不同環(huán)境下啟動(dòng)時(shí)出現(xiàn)差異的問(wèn)題,通過(guò)分析報(bào)錯(cuò)信息,發(fā)現(xiàn)是由于導(dǎo)入`spring-boot-starter-tomcat`依賴時(shí)定義的scope導(dǎo)致的配置問(wèn)題,調(diào)整依賴導(dǎo)入配置后,解決了啟動(dòng)錯(cuò)誤2024-11-11
詳解Spring Cloud Eureka多網(wǎng)卡配置總結(jié)
本篇文章主要介紹了詳解Spring Cloud Eureka多網(wǎng)卡配置總結(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04

