1.1堆空間結(jié)構(gòu)
Java 的自動內(nèi)存管理主要是針對對象內(nèi)存的回收和對象內(nèi)存的分配。同時,Java 自動內(nèi)存管理最核心的功能是 堆 內(nèi)存中對象的分配與回收。Java 堆是垃圾收集器管理的主要區(qū)域,因此也被稱作 GC 堆。Eden 區(qū)、兩個 Survivor 區(qū) S0 和 S1 都屬于新生代,中間一層屬于老年代,最下面一層屬于永久代。?
1.2內(nèi)存分配和回收機(jī)制
當(dāng) Eden 區(qū)沒有足夠空間進(jìn)行分配時,虛擬機(jī)將發(fā)起一次 Minor GC。GC 期間虛擬機(jī)又發(fā)現(xiàn) allocation1 無法存入 Survivor 空間,所以只好通過 分配擔(dān)保機(jī)制 把新生代的對象提前轉(zhuǎn)移到老年代中去。執(zhí)行 Minor GC 后,后面分配的對象如果能夠存在 Eden 區(qū)的話,還是會在 Eden 區(qū)分配內(nèi)存。
大對象直接進(jìn)入老年代,大對象就是需要大量連續(xù)內(nèi)存空間的對象。大對象直接進(jìn)入老年代主要是為了避免為大對象分配內(nèi)存時由于分配擔(dān)保機(jī)制帶來的復(fù)制而降低效率。
長期存活的對象將進(jìn)入老年代,大部分情況,對象都會首先在 Eden 區(qū)域分配。如果對象在 Eden 出生并經(jīng)過第一次 Minor GC 后仍然能夠存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間(s0 或者 s1)中,并將對象年齡設(shè)為 1。對象在 Survivor 中每熬過一次 MinorGC,年齡就增加 1 歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為 15 歲),就會被晉升到老年代中。
回收機(jī)制:
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只對新生代進(jìn)行垃圾收集;
- 老年代收集(Major GC / Old GC):只對老年代進(jìn)行垃圾收集。需要注意的是 Major GC 在有的語境中也用于指代整堆收集;
- 混合收集(Mixed GC):對整個新生代和部分老年代進(jìn)行垃圾收集。
整堆收集 (Full GC):收集整個 Java 堆和方法區(qū)。?
GC 調(diào)優(yōu)策略中很重要的一條經(jīng)驗總結(jié)是這樣說的:
將新對象預(yù)留在新生代,由于 Full GC 的成本遠(yuǎn)高于 Minor GC,因此盡可能將對象分配在新生代是明智的做法,實際項目中根據(jù) GC 日志分析新生代空間大小分配是否合理,適當(dāng)通過“-Xmn”命令調(diào)節(jié)新生代大小,最大限度降低新對象直接進(jìn)入老年代的情況。
1.3死亡對象判斷方法
引用計數(shù)法:給對象中添加一個引用計數(shù)器:有一個地方引用它,計數(shù)器就加 1;當(dāng)引用失效,計數(shù)器就減 1;這個方法實現(xiàn)簡單,效率高,但是目前主流的虛擬機(jī)中并沒有選擇這個算法來管理內(nèi)存,其最主要的原因是它很難解決對象之間相互循環(huán)引用的問題。
可達(dá)性分析算法:通過一系列的稱為 “GC Roots” 的對象作為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,節(jié)點(diǎn)所走過的路徑稱為引用鏈,當(dāng)一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的,需要被回收。
對象可以被回收,就代表一定會被回收嗎?
即使在可達(dá)性分析法中不可達(dá)的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標(biāo)記過程;可達(dá)性分析法中不可達(dá)的對象被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對象是否有必要執(zhí)行 finalize 方法。當(dāng)對象沒有覆蓋 finalize 方法,或 finalize 方法已經(jīng)被虛擬機(jī)調(diào)用過時,虛擬機(jī)將這兩種情況視為沒有必要執(zhí)行。被判定為需要執(zhí)行的對象將會被放在一個隊列中進(jìn)行第二次標(biāo)記,除非這個對象與引用鏈上的任何一個對象建立關(guān)聯(lián),否則就會被真的回收。
1.強(qiáng)引用(StrongReference)
一個對象具有強(qiáng)引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當(dāng)內(nèi)存空間不足,Java 虛擬機(jī)寧愿拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會回收強(qiáng)引用的對象。
2.軟引用(SoftReference)
如果一個對象只具有軟引用,那就類似于可有可無的生活用品。如果內(nèi)存空間足夠,垃圾回收器就不會回收它,如果內(nèi)存空間不足了,就會回收這些對象的內(nèi)存。
3.弱引用(WeakReference)
如果一個對象只具有弱引用,那就類似于可有可無的生活用品。弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間足夠與否,都會回收它的內(nèi)存。
4.虛引用(PhantomReference)
與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。虛引用主要用來跟蹤對象被垃圾回收的活動。
虛引用與軟引用和弱引用的一個區(qū)別在于: 虛引用必須和引用隊列(ReferenceQueue)聯(lián)合使用。當(dāng)垃圾回收器準(zhǔn)備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中。程序可以通過判斷引用隊列中是否已經(jīng)加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發(fā)現(xiàn)某個虛引用已經(jīng)被加入到引用隊列,那么就可以在所引用的對象的內(nèi)存被回收之前采取必要的行動。
運(yùn)行時常量池主要回收的是廢棄的常量。 字符串常量池中存在字符串 "abc",如果當(dāng)前沒有任何 String 對象引用該字符串常量的話,就說明常量 "abc" 就是廢棄常量,如果這時發(fā)生內(nèi)存回收的話而且有必要的話,"abc" 就會被系統(tǒng)清理出常量池了。
方法區(qū)主要回收的是無用的類。而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面 3 個條件才能算是 “無用的類” ,滿足可以回收。
- 該類所有的實例都已經(jīng)被回收,也就是 Java 堆中不存在該類的任何實例。
- 加載該類的 ClassLoader 已經(jīng)被回收。
- 該類對應(yīng)的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
1.4垃圾收集算法
標(biāo)記-清除算法:該算法分為“標(biāo)記”和“清除”階段:首先標(biāo)記出所有不需要回收的對象,在標(biāo)記完成后統(tǒng)一回收掉所有沒有被標(biāo)記的對象。這種垃圾收集算法會帶來兩個明顯的問題:效率問題、空間問題(標(biāo)記清除后會產(chǎn)生大量不連續(xù)的碎片)
標(biāo)記-復(fù)制算法:將內(nèi)存分為大小相同的兩塊,每次使用其中的一塊。當(dāng)這一塊的內(nèi)存使用完后,就將還存活的對象復(fù)制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內(nèi)存回收都是對內(nèi)存區(qū)間的一半進(jìn)行回收。
標(biāo)記-整理算法:根據(jù)老年代的特點(diǎn)提出的一種標(biāo)記算法,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內(nèi)存。
分代收集算法:當(dāng)前虛擬機(jī)的垃圾收集都采用分代收集算法,根據(jù)對象存活周期的不同將內(nèi)存分為幾塊。一般將 java 堆分為新生代和老年代,這樣我們就可以根據(jù)各個年代的特點(diǎn)選擇合適的垃圾收集算法。
比如在新生代中,每次收集都會有大量對象死去,所以可以選擇”標(biāo)記-復(fù)制“算法,只需要付出少量對象的復(fù)制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進(jìn)行分配擔(dān)保,所以我們必須選擇“標(biāo)記-清除”或“標(biāo)記-整理”算法進(jìn)行垃圾收集。
1.5垃圾收集器
Serial (串行)收集器:最基本、歷史最悠久的垃圾收集器了,它的 “單線程” 的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進(jìn)行垃圾收集工作的時候必須暫停其他所有的工作線程,直到它收集結(jié)束。它簡單而高效(與其他收集器的單線程相比)。Serial 收集器由于沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial 收集器對于運(yùn)行在 Client 模式下的虛擬機(jī)來說是個不錯的選擇。
ParNew 收集器:Serial 收集器的多線程版本,除了使用多線程進(jìn)行垃圾收集外,其余行為和 Serial 收集器完全一樣。新
Parallel Scavenge 收集器:收集器關(guān)注點(diǎn)是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關(guān)注點(diǎn)更多的是用戶線程的停頓時間。所謂吞吐量就是 CPU 中用于運(yùn)行用戶代碼的時間與 CPU 總消耗時間的比值。
上面都是新生代采用標(biāo)記-復(fù)制算法,老年代采用標(biāo)記-整理算法。
Serial Old 收集器:Serial 收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的后備方案。
Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本。使用多線程和“標(biāo)記-整理”算法。在注重吞吐量以及 CPU 資源的場合,都可以優(yōu)先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器。它非常符合在注重用戶體驗的應(yīng)用上使用。是 HotSpot 虛擬機(jī)第一款真正意義上的并發(fā)收集器,它第一次實現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時工作。CMS 收集器是一種 “標(biāo)記-清除”算法實現(xiàn)的,主要優(yōu)點(diǎn):并發(fā)收集、低停頓。但是它有下面三個明顯的缺點(diǎn):
- 對 CPU 資源敏感;
- 無法處理浮動垃圾;
- 它使用的回收算法-“標(biāo)記-清除”算法會導(dǎo)致收集結(jié)束時會有大量空間碎片產(chǎn)生。
?
G1 是一款面向服務(wù)器的垃圾收集器,主要針對配備多顆處理器及大容量內(nèi)存的機(jī)器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征.
- 并行與并發(fā):G1 能充分利用 CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個 CPU來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執(zhí)行的 GC 動作,G1 收集器仍然可以通過并發(fā)的方式讓 java 程序繼續(xù)執(zhí)行。
- 分代收集:雖然 G1 可以不需要其他收集器配合就能獨(dú)立管理整個 GC 堆,但是還是保留了分代的概念。
- 空間整合:與 CMS 的“標(biāo)記-清除”算法不同,G1 從整體來看是基于“標(biāo)記-整理”算法實現(xiàn)的收集器;從局部上來看是基于“標(biāo)記-復(fù)制”算法實現(xiàn)的。
- 可預(yù)測的停頓:這是 G1 相對于 CMS 的另一個大優(yōu)勢,降低停頓時間是 G1 和 CMS 共同的關(guān)注點(diǎn),但 G1 除了追求低停頓外,還能建立可預(yù)測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內(nèi)。
G1 收集器在后臺維護(hù)了一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)
到 jdk8 為止,默認(rèn)的垃圾收集器是 Parallel Scavenge 和 Parallel Old。從 jdk9 開始,G1 收集器成為默認(rèn)的垃圾收集器
1.6類文件結(jié)構(gòu)
JVM 可以理解的代碼就叫做字節(jié)碼(即擴(kuò)展名為 .class 的文件),它不面向任何特定的處理器,只面向虛擬機(jī)。Java 語言通過字節(jié)碼的方式,在一定程度上解決了傳統(tǒng)解釋型語言執(zhí)行效率低的問題,同時又保留了解釋型語言可移植的特點(diǎn)。所以 Java 程序運(yùn)行時比較高效,而且,由于字節(jié)碼并不針對一種特定的機(jī)器,因此,Java 程序無須重新編譯便可在多種不同操作系統(tǒng)的計算機(jī)上運(yùn)行。
常量池計數(shù)器是從 1 開始計數(shù)的,將第 0 項常量空出來是有特殊考慮的,索引值為 0 代表“不引用任何一個常量池項”常量池主要存放兩大常量:字面量和符號引用。
1.7類加載過程
加載:
- 通過全類名獲取定義此類的二進(jìn)制字節(jié)流
- 將字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表該類的 Class 對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口
驗證:
準(zhǔn)備:正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中分配。有以下幾點(diǎn)需要注意:
- 這時候進(jìn)行內(nèi)存分配的僅包括類變量( 靜態(tài)變量,被 static 關(guān)鍵字修飾的變量),而不包括實例變量。實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。
- 這里所設(shè)置的初始值"通常情況"下是數(shù)據(jù)類型默認(rèn)的零值(如 0、0L、null、false 等)而不是 111(初始化階段才會賦值)。
解析:虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程,也就是得到類或者字段、方法在內(nèi)存中的指針或者偏移量。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用限定符 7 類符號引用進(jìn)行。
初始化:初始化階段是執(zhí)行初始化方法,是類加載的最后一步,這一步 JVM 才開始真正執(zhí)行類中定義的 Java 程序代碼(字節(jié)碼)。<clinit> ()方法是編譯之后自動生成的。
卸載:即該類的 Class 對象被 GC。卸載類需要滿足 3 個要求:
- 該類的所有的實例對象都已被 GC,也就是說堆不存在該類的實例對象。
- 該類沒有在其他任何地方被引用
- 該類的類加載器的實例已被 GC
1.8類初始化過程
1.首先,初始化父類中的靜態(tài)成員變量和靜態(tài)代碼塊,按照在程序中出現(xiàn)的順序初始化;
2.然后,初始化子類中的靜態(tài)成員變量和靜態(tài)代碼塊,按照在程序中出現(xiàn)的順序初始化;
3.其次,初始化父類的普通成員變量和代碼塊,再執(zhí)行父類的構(gòu)造方法;
4.最后,初始化子類的普通成員變量和代碼塊,再執(zhí)行子類的構(gòu)造方法;
1.9類加載器
JVM 中內(nèi)置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現(xiàn)且全部繼承自java.lang.ClassLoader:
- BootstrapClassLoader(啟動類加載器) :最頂層的加載類,由 C++實現(xiàn),負(fù)責(zé)加載 %JAVA_HOME%/lib目錄下的 jar 包和類或者被 -Xbootclasspath參數(shù)指定的路徑中的所有類。
- ExtensionClassLoader(擴(kuò)展類加載器) :主要負(fù)責(zé)加載 %JRE_HOME%/lib/ext 目錄下的 jar 包和類,或被 java.ext.dirs 系統(tǒng)變量所指定的路徑下的 jar 包。
- AppClassLoader(應(yīng)用程序類加載器) :面向我們用戶的加載器,負(fù)責(zé)加載當(dāng)前應(yīng)用 classpath 下的所有 jar 包和類。
每一個類都有一個對應(yīng)它的類加載器。系統(tǒng)中的 ClassLoader 在協(xié)同工作的時候會默認(rèn)使用 雙親委派模型 。即在類加載的時候,系統(tǒng)會首先判斷當(dāng)前類是否被加載過。已經(jīng)被加載的類會直接返回,否則才會嘗試加載。加載的時候,首先會把該請求委派給父類加載器的 loadClass() 處理,因此所有的請求最終都應(yīng)該傳送到頂層的啟動類加載器 BootstrapClassLoader 中。當(dāng)父類加載器無法處理時,才由自己來處理。當(dāng)父類加載器為 null 時,會使用啟動類加載器 BootstrapClassLoader 作為父類加載器。
雙親委派模型的好處:雙親委派模型保證了 Java 程序的穩(wěn)定運(yùn)行,可以避免類的重復(fù)加載(JVM 區(qū)分不同類的方式不僅僅根據(jù)類名,相同的類文件被不同的類加載器加載產(chǎn)生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。
如果我們不想用雙親委派模型怎么辦?
自定義加載器的話,需要繼承 ClassLoader 。如果我們不想打破雙親委派模型,就重寫 ClassLoader 類中的 findClass() 方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。但是,如果想打破雙親委派模型則需要重寫 loadClass() 方法.打破雙親委派機(jī)制的場景有很多:JDBC、JNDI、Tomcat等
JVM白話地址
1.10JVM調(diào)優(yōu)
所有線程共享數(shù)據(jù)區(qū)大小=新生代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為 64m。所以 java 堆中增大年輕代后,將會減小年老代大小(因為老年代的清理是使用 fullgc,所以老年代過小的話反而是會增多 fullgc 的)。此值對系統(tǒng)性能影響較大,Sun 官方推薦配置為 java 堆的 3/8。
調(diào)整最大堆內(nèi)存和最小堆內(nèi)存:通常會將這兩個參數(shù)配置成相同的值,其目的是為了能夠在 java 垃圾回收機(jī)制清理完堆區(qū)后不需要重新分隔計算堆區(qū)的大小而浪費(fèi)資源。
調(diào)整新生代和老年代的比值。
調(diào)整 Survivor 區(qū)和 Eden 區(qū)的比值。
設(shè)置年輕代和老年代的大小。文章來源:http://www.zghlxwxcb.cn/news/detail-425719.html
根據(jù)實際事情調(diào)整新生代和幸存代的大小,官方推薦新生代占 java 堆的 3/8,幸存代占新生代的 1/10。文章來源地址http://www.zghlxwxcb.cn/news/detail-425719.html
到了這里,關(guān)于java垃圾回收機(jī)制(面試)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!