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