JVM解密之解構(gòu)類加載與GC垃圾回收機(jī)制詳解
一. JVM內(nèi)存劃分
JVM 其實(shí)是一個(gè) Java 進(jìn)程,該進(jìn)程會(huì)從操作系統(tǒng)中申請(qǐng)一大塊內(nèi)存區(qū)域,提供給 Java 代碼使用,申請(qǐng)的內(nèi)存區(qū)域會(huì)進(jìn)一步做出劃分,給出不同的用途。
其中最核心的是棧,堆,方法區(qū)這幾個(gè)區(qū)域:
- 堆,用來(lái)放置 new 出來(lái)的對(duì)象,類成員變量。
- 棧,維護(hù)方法之間的調(diào)用關(guān)系,放置局部變量。
- 方法區(qū)(舊)/元數(shù)據(jù)區(qū)(新):放的是類加載之后的類對(duì)象(
.class
文件),靜態(tài)變量,二進(jìn)制指令(方法)。
細(xì)分下來(lái) JVM 的內(nèi)存區(qū)域包括以下幾個(gè):程序計(jì)數(shù)器,棧,堆,方法區(qū),圖中的元數(shù)據(jù)區(qū)可以理解為方法區(qū)。
程序計(jì)數(shù)器:內(nèi)存最小的一塊區(qū)域,保存了下一條要執(zhí)行的指令(字節(jié)碼)的地址,每個(gè)線程都有一份。
棧:儲(chǔ)存局部變量與方法之間的調(diào)用信息,每一個(gè)線程都有一份,但要注意“棧是線程私有的”這種說(shuō)法是不準(zhǔn)確的,私有的意思是我的你是用不了的,但實(shí)際上,一個(gè)線程棧上的內(nèi)容,是可以被另一個(gè)線程使用到的。
棧在 JVM 區(qū)域劃分中分為兩種,一種是 Java 虛擬機(jī)棧,另外一種是本地方法棧,這兩種棧功能非常類似,當(dāng)方法被調(diào)用時(shí),都會(huì)同步創(chuàng)建棧幀來(lái)存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法出口等信息。
只不過(guò)虛擬機(jī)棧是為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是給 JVM 內(nèi)部的本地(Native)方法服務(wù)的(JVM 內(nèi)部通過(guò) C++ 代碼實(shí)現(xiàn)的方法)。
堆:儲(chǔ)存對(duì)象以及對(duì)象的成員變量,一個(gè) JVM 進(jìn)程只有一個(gè),多個(gè)線程共用一個(gè)堆,是內(nèi)存中空間最大的區(qū)域,Java 堆是垃圾回收器管理的內(nèi)存區(qū)域,后文介紹 GC 的時(shí)候細(xì)說(shuō)。
方法區(qū): JDK 1.8 開(kāi)始,叫做元數(shù)據(jù)區(qū),存儲(chǔ)了類對(duì)象,常量池,靜態(tài)成員變量,即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù);所謂的“類對(duì)象”,就是被static
修飾的變量或方法就成了類屬性,.java
文件會(huì)被編譯成.class
文件,.class
會(huì)被加載到內(nèi)存中,也就被 JVM 構(gòu)造成類對(duì)象了,類對(duì)象描述了類的信息,如類名,類有哪些成員,每個(gè)成員叫什么名字,權(quán)限是什么,方法名等;同樣一個(gè) JVM 進(jìn)程只有一個(gè)元數(shù)據(jù)區(qū),多個(gè)線程共用一塊元數(shù)據(jù)區(qū)內(nèi)存。
要注意 JVM 的線程和操作系統(tǒng)的線程是一對(duì)一的關(guān)系,每次在 Java 代碼中創(chuàng)建的線程,必然會(huì)在系統(tǒng)中有一個(gè)對(duì)應(yīng)的線程。
二. 類加載機(jī)制
1. 類加載過(guò)程
類加載就是把.java
文件使用javac
編譯為.class
文件,從文件(硬盤)被加載到內(nèi)存中(元數(shù)據(jù)區(qū)),得到類對(duì)象的過(guò)程。(程序要想運(yùn)行,就需要把依賴的“指令和數(shù)據(jù)”加載到內(nèi)存中)。
這個(gè)圖片所示的類加載過(guò)程來(lái)自官方文檔,類加載包括三個(gè)步驟:Loading
, Linking
, Initialization
。
官方文檔:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html
下面就來(lái)了解一下這三步是在干什么:
第一步,加載(Loading
),找到對(duì)應(yīng)的.class
文件,打開(kāi)并讀取文件到內(nèi)存中,同時(shí)通過(guò)解析文件初步生成一個(gè)代表這個(gè)類的 java.lang.Class 對(duì)象。
第二步,連接(Linking
),作用是建立多個(gè)實(shí)體之間的聯(lián)系,該過(guò)程有包含三個(gè)小過(guò)程:
- 驗(yàn)證(
Verification
),主要就是驗(yàn)證讀取到的內(nèi)容是不是和規(guī)范中規(guī)定的格式完全匹配,如果不匹配,那么類加載失敗,并且會(huì)拋出異常;一個(gè).class
文件的格式如下:
- 通過(guò)觀察
.class
文件結(jié)構(gòu),其實(shí).class
文件把.java
文件的核心信息都保留了下來(lái),只不過(guò)是使用二進(jìn)制的方式重新進(jìn)行組織了,.class
文件是二進(jìn)制文件,這里的格式有嚴(yán)格說(shuō)明的,哪幾個(gè)字節(jié)表示什么,java官方文檔都有明確規(guī)定。 來(lái)自官方文檔:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1 - 準(zhǔn)備(
Preparation
),給類對(duì)象分配內(nèi)存空間(先在元數(shù)據(jù)區(qū)占個(gè)位置),并為類中定義的靜態(tài)變量分配內(nèi)存,此時(shí)類變量初始值也就都為 0 值了。 - 解析(
Resolution
),針對(duì)字符串常量初始化,將符號(hào)引用轉(zhuǎn)為直接引用;字符串常量,得有一塊內(nèi)存空間,存這個(gè)字符的實(shí)際內(nèi)容,還得有一個(gè)引用來(lái)保存這個(gè)內(nèi)存空間的起始地址;在類加載之前,字符串常量是在.class
文件中的,此時(shí)這個(gè)引用記錄的并非是字符串常量真正的地址,而是它在文件的偏移量/占位符(符號(hào)引用),也就是說(shuō),此時(shí)常量之間只是知道它們彼此之間的相對(duì)位置,不知道自己在內(nèi)存中的實(shí)際地址;在類加載之后,才會(huì)真正的把這個(gè)字符串常量給填充到特定的內(nèi)存地址上中,這個(gè)引用才能被真正賦值成指定內(nèi)存地址(直接引用),此時(shí)字符串常量之間相對(duì)位置還是一樣的;這個(gè)場(chǎng)景可以想象你看電影時(shí)拿著電影票入場(chǎng)入座。
第三步,初始化(Initialization),這里是真正地對(duì)類對(duì)象進(jìn)行初始化,特別是靜態(tài)成員,調(diào)用構(gòu)造方法,進(jìn)行成員初始化,執(zhí)行代碼塊,靜態(tài)代碼塊,加載父類…
類加載的時(shí)機(jī):
類加載并不是 Java 程序(JVM)一運(yùn)行就把所有類都加載了,而是真正用到哪個(gè)類才加載哪個(gè);整體是一個(gè)“懶加載”的策略;只有需要用的時(shí)候才加載(非必要,不加載),就會(huì)觸發(fā)以下的加載:
- 構(gòu)造類的實(shí)例
- 調(diào)用這個(gè)類的靜態(tài)方法/使用靜態(tài)屬性
- 加載子類就會(huì)先加載其父類
一旦加載過(guò)后后續(xù)使用就不必加載了。
2. 雙親委派模型
雙親委派模型是類加載中的一個(gè)環(huán)節(jié),屬于加載階段,它是描述如何根據(jù)類的全限定名找到.class
文件的過(guò)程。
在 JVM 里面提供了一組專門的對(duì)象,用來(lái)進(jìn)行類的加載,即類加載器,當(dāng)然既然雙親委派模型是類加載中的一部分,所以其所描述找.class
文件的過(guò)程也是類加載器來(lái)負(fù)責(zé)的。
但是想要找全.class
文件可不容易,畢竟.class
文件可能在 jdk 目錄里面,可能在項(xiàng)目的目錄里面,還可能在其他特定的位置,因此 JVM 提供了多個(gè)類加載器,每一個(gè)類加載器負(fù)責(zé)在一個(gè)片區(qū)里面找。
默認(rèn)的類加載器主要有三個(gè):
- BootStrapClassLoader,負(fù)責(zé)加載 Java 標(biāo)準(zhǔn)庫(kù)里面的類,如 String,Random,Scanner 等。
- ExtensionClassLoader,負(fù)責(zé)加載 JVM 擴(kuò)展庫(kù)中的類,是規(guī)范之外,由實(shí)現(xiàn) JVM 的組織(Sun/Oracle),提供的額外的功能。
- ApplicationClassLoader,負(fù)責(zé)加載當(dāng)前項(xiàng)目目錄中自己寫的類以及第三方庫(kù)中的類。
除了默認(rèn)的幾個(gè)類加載器,程序員還可以自定義類加載器,來(lái)加載其他目錄的類,此時(shí)也不是非要遵守雙親委派模型,如 Tomcat 就自定義了類加載器,用來(lái)專門加載webapps
目錄中的.class
文件就沒(méi)有遵守。
雙親委派模型就描述了類加載過(guò)程中的找目錄的環(huán)節(jié),它的過(guò)程如下:
如果一個(gè)類加載器收到了類加載的請(qǐng)求,首先需要先給定一個(gè)類的全限定類名,如:“java.lang.String”。
根據(jù)類的全限定名找的過(guò)程中它不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此。
因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載(去自己的片區(qū)搜索)。
舉個(gè)例子:我們要去找標(biāo)準(zhǔn)庫(kù)里面的String.class
文件,它的過(guò)程大致如下:
- 首先
ApplicationClassLoader
類收到類加載請(qǐng)求,但是它先詢問(wèn)父類加載器是否加載過(guò),即詢問(wèn)ExtensionClassLoader
類是否加載過(guò)。 - 如果
ExtensionClassLoader
類沒(méi)有加載過(guò),請(qǐng)求就會(huì)向上傳遞到ExtensionClassLoader
類,然后同理,詢問(wèn)它的父加載器BootstrapClassLoader
是否加載過(guò)。 - 如果
BootstrapClassLoader
沒(méi)有加載過(guò),則加載請(qǐng)求就會(huì)到BootstrapClassLoader
加載器這里,由于BootstrapClassLoader
加載器是最頂層的加載器,它就會(huì)去標(biāo)準(zhǔn)庫(kù)進(jìn)行搜索,看是否有String
類,我們知道String
是在標(biāo)準(zhǔn)庫(kù)中的,因此可以找到,請(qǐng)求的加載任務(wù)完成,這個(gè)過(guò)程也就結(jié)束了。
再比如,這里要加載我自己寫的的Test
類,過(guò)程如下:
- 首先
ApplicationClassLoader
類收到類加載請(qǐng)求,但是它先詢問(wèn)父類加載器是否加載過(guò),即詢問(wèn)ExtensionClassLoader
類是否加載過(guò)。 - 如果
ExtensionClassLoader
類沒(méi)有加載過(guò),請(qǐng)求就會(huì)向上傳遞到ExtensionClassLoader
類,然后同理,詢問(wèn)它的父加載器BootstrapClassLoader
是否加載過(guò)。 - 如果
BootstrapClassLoader
沒(méi)有加載過(guò),則加載請(qǐng)求就會(huì)到BootstrapClassLoader
加載器這里,由于BootstrapClassLoader
加載器是最頂層的加載器,它就會(huì)去標(biāo)準(zhǔn)庫(kù)進(jìn)行搜索,看是否有Test
類,我們知道Test類不在標(biāo)準(zhǔn)庫(kù),所以會(huì)回到子加載器里面搜索。 - 同理,
ExtensionClassLoader
加載器也沒(méi)有Test
類,會(huì)繼續(xù)向下,到ApplicationClassLoader
加載器中尋找,由于ApplicationClassLoader
加載器搜索的就是項(xiàng)目目錄,因此可以找到Test
類,全過(guò)程結(jié)束。
如果在ApplicationClassLoader
還沒(méi)有找到,就會(huì)拋出異常。
總的來(lái)說(shuō),雙親委派模型就是找.class
文件的過(guò)程,其實(shí)也沒(méi)啥,就是名字挺哄人。
之所以有上述的查找順序,大概是因?yàn)?JVM 代碼是按照類似于遞歸的方式來(lái)實(shí)現(xiàn)的,就導(dǎo)致了從下到上,又從上到下過(guò)程,這個(gè)順序,最主要的目的,就是為了保證 Bootstrap 能夠先加載,Application 能夠后加載,這就可以避免說(shuō)因?yàn)橛脩魟?chuàng)建了一些奇怪的類,引起不必要的 bug。
三. GC垃圾回收機(jī)制
在 C/C++ 中內(nèi)存空間是需要進(jìn)行手動(dòng)釋放,如果沒(méi)有手動(dòng)去釋放那么這塊內(nèi)存空間就會(huì)持續(xù)存在,一直到進(jìn)程結(jié)束,并且堆的內(nèi)存生命周期比較長(zhǎng),不像棧隨著方法執(zhí)行結(jié)束自動(dòng)銷毀釋放,堆默認(rèn)是不能自動(dòng)釋放的,這就可能導(dǎo)致內(nèi)存泄露的問(wèn)題,進(jìn)一步導(dǎo)致后續(xù)的內(nèi)存申請(qǐng)操作失敗。
而在 Java 中引入了 GC 垃圾回收機(jī)制,垃圾指的是我們不再使用的內(nèi)存,垃圾回收就是把我們不用的內(nèi)存自動(dòng)釋放了。
GC的好處:
- 非常省心,使程序員寫代碼更簡(jiǎn)單一些,不容易出錯(cuò)。
GC的壞處:
- 需要消耗額外的系統(tǒng)資源,也有額外的性能開(kāi)銷。
- GC 這里還有一個(gè)嚴(yán)重的 STW(stop the world)問(wèn)題,如果有時(shí)候,內(nèi)存中的垃圾已經(jīng)很多了,這個(gè)時(shí)候觸發(fā)一次 GC 就會(huì)消耗大量系統(tǒng)資源,其他程序可能就無(wú)法正常執(zhí)行了;GC 可能會(huì)涉及一些鎖操作,就可能導(dǎo)致業(yè)務(wù)代碼無(wú)法正常執(zhí)行;極端情況下可會(huì)卡頓幾十毫秒甚至上百毫秒。
GC 的實(shí)際工作過(guò)程包含兩部分:
- 找到/判定垃圾。
- 再進(jìn)行垃圾的釋放。
1. 找到需要回收的內(nèi)存
1.1 哪些內(nèi)存需要回收?
Java 程序運(yùn)行時(shí),內(nèi)存分為四個(gè)區(qū),分別是程序計(jì)數(shù)器,棧,堆,方法區(qū)。
對(duì)于程序計(jì)數(shù)器,它占據(jù)固定大小的內(nèi)存,它是隨著線程一起銷毀的,不涉及釋放,那么也就用不到 GC;對(duì)于??臻g,函數(shù)執(zhí)行完畢,對(duì)應(yīng)的棧幀自動(dòng)銷毀釋放了,也不需要 GC;對(duì)于方法區(qū),主要進(jìn)行類加載,雖然需要進(jìn)行“類卸載”,此時(shí)需要釋放內(nèi)存,但是這個(gè)操作的頻率是非常低的;最后對(duì)于堆空間,經(jīng)常需要釋放內(nèi)存,GC 也是主要針對(duì)堆進(jìn)行釋放的。
在堆空間,內(nèi)存的分布有三種,一是正在使用的內(nèi)存,二是不用了但未回收的內(nèi)存,三是未分配的內(nèi)存,那內(nèi)存中的對(duì)象,也有三種情況,對(duì)象內(nèi)存全部在使用(相當(dāng)于對(duì)象整體全部在使用),對(duì)象的內(nèi)存部分在使用(相當(dāng)于對(duì)象的一部分在使用),對(duì)象的內(nèi)存不使用(對(duì)象也就使用完畢了),對(duì)于這三類對(duì)象,前兩類不需要回收,只有最后一類是需要回收的。
所以,垃圾回收的基本單位是對(duì)象,而不是字節(jié),對(duì)于如何找到垃圾,常用有引用計(jì)數(shù)法與可達(dá)性分析法兩種方式,關(guān)鍵思路是,抓住這個(gè)對(duì)象,看看到底有沒(méi)有“引用”指向它,沒(méi)有引用了,它就是需要被釋放的垃圾。
1.2 基于引用計(jì)數(shù)找垃圾(Java不采取該方案)
所謂基于引用計(jì)數(shù)判斷垃圾,就是給每一個(gè)對(duì)象分配一個(gè)計(jì)數(shù)器(整數(shù)),來(lái)記錄該對(duì)象被多少個(gè)引用變量所指,每次創(chuàng)建一個(gè)引用指向該對(duì),,計(jì)數(shù)器就+1
,每次該引用被銷毀了計(jì)數(shù)器就–1
,如果這個(gè)計(jì)數(shù)器的值為0
則表示該對(duì)象需要回收,比如有一個(gè)Test對(duì)象,它被三個(gè)引用所指,所以這個(gè) Test 對(duì)象所帶計(jì)數(shù)器的值就是3
。
//偽代碼: Test t1 = new Test(); Test t2 = t1; Test t3 = t1;
如果上述的偽代碼是在一個(gè)方法中,待方法執(zhí)行完畢,方法中的局部引用變量被銷毀,那么Test對(duì)象的引用計(jì)數(shù)變?yōu)?code>0,此時(shí)就會(huì)被回收。
由此可見(jiàn),基于引用計(jì)數(shù)的方案非常簡(jiǎn)單高效并且可靠,但是它擁有兩個(gè)致命缺陷:
- 內(nèi)存空間浪費(fèi)較多(利用率低), 需要給每個(gè)對(duì)象分配一個(gè)計(jì)數(shù)器,如果按照4個(gè)字節(jié)來(lái)算;代碼中的對(duì)象非常少時(shí)無(wú)所謂,但如果對(duì)象特別多了,占用的額外空間就會(huì)很多,尤其是每個(gè)對(duì)象都比較小的情況下。
- 存在循環(huán)引用的問(wèn)題,會(huì)出現(xiàn)對(duì)象既不使用也不釋放的情況,看下面舉例子來(lái)分析一下。
有以下一段偽代碼:
class Test { Test t = null; } //main方法中: Test t1 = new Test(); // 1號(hào)對(duì)象, 引用計(jì)數(shù)是1 Test t2 = new Test(); // 2號(hào)對(duì)象, 引用計(jì)數(shù)是1 t1.t = t2; // t1.t指向2號(hào)對(duì)象, 此時(shí)2號(hào)對(duì)象引用計(jì)數(shù)是2 t2.t = t1; // t1.t指向1號(hào)對(duì)象, 此時(shí)1號(hào)對(duì)象引用計(jì)數(shù)是2
執(zhí)行上述偽代碼,運(yùn)行時(shí)內(nèi)存圖如下:
然后,我們把變量t1
與t2置為null
,偽代碼如下:
//偽代碼: t1 = null; t2 = null;
執(zhí)行完上面?zhèn)未a,運(yùn)行時(shí)內(nèi)存圖如下:
此時(shí) t1 和 t2 引用銷毀了,一號(hào)對(duì)象和二號(hào)對(duì)象的引用計(jì)數(shù)都-1
,但由于兩個(gè)對(duì)象的屬性相互指向另一個(gè)對(duì)象,計(jì)數(shù)器結(jié)果都是1
而不是0
造成對(duì)象無(wú)法及時(shí)得到釋放,而實(shí)際上這個(gè)兩個(gè)對(duì)象已經(jīng)獲取不到了(應(yīng)該銷毀了)。
1.3 基于可達(dá)性分析找垃圾(Java采取方案)
Java 中的對(duì)象都是通過(guò)引用來(lái)指向并訪問(wèn)的,一個(gè)引用指向一個(gè)對(duì)象,對(duì)象里的成員又指向別的對(duì)象。
所謂可達(dá)性分析,就是通過(guò)額外的線程,將整個(gè) Java 程序中的對(duì)象用鏈?zhǔn)?樹(shù)形結(jié)構(gòu)把所有對(duì)象串起來(lái),從根節(jié)點(diǎn)出發(fā)去遍歷這個(gè)樹(shù)結(jié)構(gòu),所有能訪問(wèn)到的對(duì)象,標(biāo)記成“可達(dá)”,不能訪問(wèn)到的,就是“不可達(dá)”,JVM 有一個(gè)所有對(duì)象的名單(每 new 一個(gè)對(duì)象,JVM 都會(huì)記錄下來(lái),JVM 就會(huì)知道一共有哪些對(duì)象,每個(gè)對(duì)象的地址是什么),通過(guò)上述遍歷,將可達(dá)的標(biāo)記出來(lái),剩下的不可達(dá)的(未標(biāo)記的)就可以作為垃圾進(jìn)行回收了。
可達(dá)性分析的起點(diǎn)稱為GC Roots
(就是一個(gè)Java對(duì)象),一個(gè)代碼中有很多這樣的起點(diǎn),把每個(gè)起點(diǎn)都遍歷一遍就完成了一次掃描。
對(duì)于這個(gè)GCRoots
,一般很難被回收,它來(lái)源可以分為以下幾種:
- 在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象,例如各個(gè)線程被調(diào)用的方法堆棧中使用到的參數(shù)、局部變量、臨時(shí)變量等。
- 在本地方法棧中 JNI(即通常所說(shuō)的Native方法)引用的對(duì)象。
- 常量池中引用所指向的對(duì)象。
- 方法區(qū)中靜態(tài)成員所指向的對(duì)象。
- 所有被同步鎖(synchronized 關(guān)鍵字)持有的對(duì)象。
可達(dá)性分析克服了引用計(jì)數(shù)的兩個(gè)缺點(diǎn),但它有自己的問(wèn)題:
- 需要進(jìn)行類似于 “樹(shù)遍歷”的過(guò)程,消耗更多的時(shí)間,但可達(dá)性分析操作并不需要一直執(zhí)行,只需要隔一段時(shí)間執(zhí)行一次尋找不可達(dá)對(duì)象,確定垃圾就可以,所以,慢一下點(diǎn)也是沒(méi)關(guān)系的,雖遲,但到。
- 可達(dá)性分析過(guò)程,當(dāng)前代碼中的對(duì)象的引用關(guān)系發(fā)生變化了,還比較麻煩,所以為了準(zhǔn)確的完成這個(gè)過(guò)程,就需要讓其他的業(yè)務(wù)暫停工作(STW問(wèn)題),但 Java 發(fā)展這么多年,垃圾回收機(jī)制也在不斷的更新優(yōu)化,STW 這個(gè)問(wèn)題,現(xiàn)在已經(jīng)能夠比較好的應(yīng)對(duì)了,雖不能完全消除,但也已經(jīng)可以讓 STW 的時(shí)間盡量短了。
2. 垃圾回收算法
垃圾回收的算法最常見(jiàn)的有以下幾種:
- 標(biāo)記-清除算法
- 標(biāo)記-復(fù)制算法
- 標(biāo)記-整理算法
- 分代回收算法(本質(zhì)就是綜合上述算法,在堆的不同區(qū)采取不同的策略)
2.1 標(biāo)記-清除算法
標(biāo)記其實(shí)就是可達(dá)性分析的過(guò)程,在可達(dá)性分析的過(guò)程中,會(huì)標(biāo)記可達(dá)的對(duì)象,其不可達(dá)的對(duì)象,都會(huì)被視為垃圾進(jìn)行回收。
比如經(jīng)過(guò)一輪標(biāo)記后,標(biāo)記狀態(tài)和回收后狀態(tài)如圖:
我們發(fā)現(xiàn),內(nèi)存是釋放了,但是回收后,未分配的內(nèi)存空間是零散的不是連續(xù)的,我們知道申請(qǐng)內(nèi)存的時(shí)候得到的內(nèi)存得是連續(xù)的,雖然內(nèi)存釋放后總的空閑空間很大,但由于未分配的內(nèi)存是碎片化的,就有可能申請(qǐng)內(nèi)存失??;假設(shè)你的主機(jī)有 1GB 空閑內(nèi)存,但是這些內(nèi)存是碎片形式存在的,當(dāng)申請(qǐng) 500MB 內(nèi)存的時(shí)候,也可能會(huì)申請(qǐng)失敗,畢竟不能保證有一塊大于 500MB 的連續(xù)內(nèi)存空間,這也是標(biāo)記-清除算法的缺陷(內(nèi)存碎片問(wèn)題)。
2.2 標(biāo)記-復(fù)制算法
為了解決標(biāo)記-清除算法所帶來(lái)的內(nèi)存碎片化的問(wèn)題,引入了復(fù)制算法。
它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊,每次清理,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的這一塊內(nèi)存空間一次清理掉。
復(fù)制算法的第一步還是要通過(guò)可達(dá)性分析進(jìn)行標(biāo)記,得到哪一部分需要進(jìn)行回收,哪一部分需要保留,不能回收。
標(biāo)記完成后,會(huì)將還在使用的內(nèi)存連續(xù)復(fù)制到另外一塊等大的內(nèi)存上,這樣得到的未分配內(nèi)存一直都是連續(xù)的,而不是碎片化的。
但是,復(fù)制算法也有缺陷:
- 空間利用率低。
- 如果垃圾少,有效對(duì)象多,復(fù)制成本就比較大。
2.3 標(biāo)記-整理算法
標(biāo)記-整理算法針對(duì)復(fù)制算法做出進(jìn)一步改進(jìn),其中的標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一致,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。
回收時(shí)是將存活對(duì)象按照某一順序(比如從左到右,從上到下的順序)拷貝到非存活對(duì)象的內(nèi)存區(qū)域,類似于順序表的刪除操作,會(huì)將后面的元素搬運(yùn)到前面。
解決了標(biāo)記-復(fù)制算法空間利用率低的問(wèn)題,也沒(méi)有內(nèi)存碎片的問(wèn)題,但是復(fù)制的開(kāi)銷問(wèn)題并沒(méi)有得到解決。
2.4 分代回收
上述的回收算法都有一定的缺陷,分代回收就是將上述三種算法結(jié)合起來(lái)分區(qū)使用,分代回收會(huì)針對(duì)對(duì)象進(jìn)行分類,以熬過(guò)的 GC 掃描輪數(shù)作為“年齡”,然后針對(duì)不同年齡采取不同的方案。
分代是基于一個(gè)經(jīng)驗(yàn)規(guī)律,如果一個(gè)東西存在時(shí)間長(zhǎng)了,那么接下來(lái)大概率也會(huì)存在(要沒(méi)有早就沒(méi)有了)。
我們知道 GC 主要是回收堆上的無(wú)用內(nèi)存,我們先來(lái)了解一下堆的劃分,堆包括新生代(Young)、老年代(Old),而新生代包括一個(gè)伊甸區(qū)(Eden)與兩個(gè)幸存區(qū)(Survivor),分代回收算法就會(huì)根據(jù)不同的代去采取不同的標(biāo)記-xx算法。
在新生代,包括一個(gè)伊甸區(qū)與兩個(gè)幸存區(qū),伊甸區(qū)存儲(chǔ)的是未經(jīng)受 GC 掃描的對(duì)象(年齡為 0),也就是剛剛 new 出來(lái)的對(duì)象。
幸存區(qū)存儲(chǔ)了經(jīng)過(guò)若干輪 GC 掃描的對(duì)象,通過(guò)實(shí)際經(jīng)驗(yàn)得出,大部分的 Java 對(duì)象具有“朝生夕滅”的特點(diǎn),生命周期非常短,也就是說(shuō)只有少部分的伊甸區(qū)對(duì)象才能熬過(guò)第一輪的 GC 掃描到幸存區(qū),所以到幸存區(qū)的對(duì)象相比于伊甸區(qū)少的多,正因?yàn)榇蟛糠中律膶?duì)象熬不過(guò) GC 第一輪掃描,所以伊甸區(qū)與幸存區(qū)的分配比例并不是1:1
的關(guān)系,HotSpot 虛擬機(jī)默認(rèn)一個(gè) Eden 和一個(gè) Survivor 的大小比例是 8∶1,正因?yàn)樾律拇婊盥瘦^小,所以新生代使用的垃圾回收算法為標(biāo)記-復(fù)制算法最優(yōu),畢竟存活率越小,對(duì)于標(biāo)記-復(fù)制算法,復(fù)制的開(kāi)銷也就很小。
不妨我們將第一個(gè) Survivor 稱為活動(dòng)空間,第二個(gè) Survivor 稱為空閑空間,一旦發(fā)生 GC,會(huì)將 10% 的活動(dòng)區(qū)間與另外 80% 伊甸區(qū)中存活的對(duì)象復(fù)制到 10% 的空閑空間,接下來(lái),將之前 90% 的內(nèi)存全部釋放,以此類推。
在后續(xù)幾輪 GC 中,幸存區(qū)對(duì)象在兩個(gè) Survivor 中進(jìn)行標(biāo)記-復(fù)制算法,此處由于幸存區(qū)體積不大,浪費(fèi)的空間也是可以接受的。
在繼續(xù)持續(xù)若干輪 GC 后(這個(gè)對(duì)象已經(jīng)再兩個(gè)幸存區(qū)中來(lái)回考貝很多次了),幸存區(qū)的對(duì)象就會(huì)被轉(zhuǎn)移到老年代,老年代中都是年齡較老的對(duì)象,根據(jù)經(jīng)驗(yàn),一個(gè)對(duì)象越老,繼續(xù)存活的可能性就越大(要掛早掛了),因此老年代的 GC 掃描頻率遠(yuǎn)低于新生代,所以老年代采用標(biāo)記-整理的算法進(jìn)行內(nèi)存回收,畢竟老年代存活率高,對(duì)于標(biāo)記-整理算法,復(fù)制轉(zhuǎn)移的開(kāi)銷很低。
還要注意一個(gè)特殊情況,如果對(duì)象非常大,就直接進(jìn)入老年代,因?yàn)榇髮?duì)象進(jìn)行復(fù)制算法,成本比較高,而且大對(duì)象也不會(huì)很多。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺談Java消息隊(duì)列總結(jié)篇(ActiveMQ、RabbitMQ、ZeroMQ、Kafka)
這篇文章主要介紹了淺談Java消息隊(duì)列總結(jié)篇(ActiveMQ、RabbitMQ、ZeroMQ、Kafka),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-05-05java并發(fā)編程工具類JUC之LinkedBlockingQueue鏈表隊(duì)列
大家都知道LinkedBlockingQueue 隊(duì)列是BlockingQueue接口的實(shí)現(xiàn)類,所以它具有BlockingQueue接口的一切功能特點(diǎn),他還提供了兩種構(gòu)造函數(shù),本文中通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-06-06Java8接口中引入default關(guān)鍵字的本質(zhì)原因詳析
Default方法是在java8中引入的關(guān)鍵字,也可稱為Virtual extension methods—虛擬擴(kuò)展方法,這篇文章主要給大家介紹了關(guān)于Java8接口中引入default關(guān)鍵字的本質(zhì)原因,需要的朋友可以參考下2022-01-01java實(shí)現(xiàn)動(dòng)態(tài)上傳多個(gè)文件并解決文件重名問(wèn)題
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)動(dòng)態(tài)上傳多個(gè)文件,并解決文件重名問(wèn)題的方法,感興趣的小伙伴們可以參考一下2016-03-03Spring Boot集成tablesaw插件快速入門示例代碼
Tablesaw是一款Java的數(shù)據(jù)可視化庫(kù),數(shù)據(jù)解析庫(kù),主要用于加載數(shù)據(jù),對(duì)數(shù)據(jù)進(jìn)行操作(轉(zhuǎn)化,過(guò)濾,匯總等),類比Python中的Pandas庫(kù),本文介紹Spring Boot集成tablesaw插件快速入門Demo,感興趣的朋友一起看看吧2024-06-06spring cloud整合ribbon問(wèn)題及解決方案
很多小伙伴在整合ribbon都出了相同的問(wèn)題,今天特地為大家整理了該問(wèn)題的解決方案,文中有非常詳細(xì)的圖文解說(shuō),對(duì)出現(xiàn)同樣問(wèn)題的小伙伴們很有幫助,需要的朋友可以參考下2021-05-05Java對(duì)象深復(fù)制與淺復(fù)制實(shí)例詳解
這篇文章主要介紹了 Java對(duì)象深復(fù)制與淺復(fù)制實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-05-05Spring Boot Admin Server管理客戶端過(guò)程詳解
這篇文章主要介紹了Spring Boot Admin Server管理客戶端過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03