常見面試題
- 如何判斷對象是否死亡
- 簡單介紹一下強(qiáng)引用、軟引用、弱引用、虛引用
- 如何判斷常量是一個廢棄常量
- 如何判斷類是一個無用類
- 垃圾收集有哪些算法、各自的特點(diǎn)?
- 常見的垃圾回收器有哪些?
- 介紹一下CMS,G1收集器?
- minor gc和full gc有什么不同呢?
1.JVM內(nèi)存回收和分配
1.1主要的區(qū)域?
- 在伊甸區(qū)先產(chǎn)生對象
- 然后發(fā)生一次gc之后去到幸存區(qū)幸存區(qū)
- 如果年齡大于閾值那么就會升級到老年代
閾值的計算
如果某個年齡段的大小大于幸存區(qū)的一半,那么就取閾值或者是這個年齡最小的那個作為新的閾值升級到老年代
- gc的時候是幸存區(qū)的from和伊甸區(qū)的存活對象復(fù)制到to,然后再清理其它的對象,接著from和to就會交換指針
gc測試
場景就是先給eden分配足量的空間,然后再申請大量空間,問題就是幸存區(qū)的空間不夠用
- 那么這個時候就會觸發(fā)分配擔(dān)保機(jī)制,把多余的對象分配到老年代,而不會觸發(fā)full gc。仍然還是monor gc
public class GCTest { public static void main(String[] args) { byte[] allocation1, allocation2; allocation1 = new byte[50900*1024]; allocation2 = new byte[9500*1024]; } }
1.2大對象進(jìn)入老年代
- 防止在標(biāo)記復(fù)制的時候占用大量的時間,降低gc的效率
1.3長期存活的對象進(jìn)入老年代
- 每次gc都會把eden和from的存活對象放到to,每次gc存活年齡就會+1,如果超過閾值那么就能夠升級到老年代,設(shè)置的參數(shù)是-XX:MaxTenuringThreshold
- 下面是計算的方式,每個年齡的人數(shù)累加,累加一個就+1,如果對象數(shù)量大于幸存區(qū)的一半的時候就需要更新閾值(新計算的age和MaxTenuringThreshold)
- 通常晉升閾值是15,但是CMS是6
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { //survivor_capacity是survivor空間的大小 size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100); size_t total = 0; uint age = 1; while (age < table_size) { //sizes數(shù)組是每個年齡段對象大小 total += sizes[age]; if (total > desired_survivor_size) { break; } age++; } uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; ... }
1.4主要進(jìn)行g(shù)c的區(qū)域
gc的類型
- Partial Gc
Young Gc:收集新生代的
Old Gc:只收集老年代的
Mixed Gc:新生代和部分老年代
- Full Gc:新生代,老年代都會收集
Young Gc
- 每次都是收集新生代的,并且晉升那些存活久的
Full Gc
- 如果發(fā)現(xiàn)幸存區(qū)要晉升的對象內(nèi)存空間比老年代內(nèi)存空間更大那么就進(jìn)行full Gc。有的虛擬機(jī)會先進(jìn)行young gc來清理掉一些,減少full gc的時間消耗
1.5空間分配擔(dān)保?
- jdk1.6之前需要判斷老年代剩余的空間是不是完全大于新生代的空間,如果是那么才能進(jìn)行minorgc保證不會出現(xiàn)問題。如果是不行就會去檢查-XX:handlePromotionFailure也就是晉升的對象平均大小是不是小于老年代剩余空間,如果是那么就直接minor gc否則就full gc
- jdk1.6之后直接檢查新生代晉升平均大小如果小于老年代那么就會直接晉升
2.對象已經(jīng)死亡?
2.1引用計數(shù)法
- 其實(shí)就是每次被引用那么計數(shù)+1,如果計數(shù)不是0那么就不會被回收
- 但是不使用的原因就是循環(huán)引用依賴,如果兩個對象互相引用就會導(dǎo)致計數(shù)永遠(yuǎn)不會為0
2.2可達(dá)性分析
- Gc roots作為起點(diǎn)一直往下面的一條引用鏈
gc Roots的對象
- 虛擬機(jī)棧引用的對象(棧的本地局部變量表)
- 本地方法棧引用的對象
- 方法區(qū)常量引用的對象(常量池引用的對象)
- 方法區(qū)靜態(tài)屬性引用的對象
- 被同步鎖持有的對象
- java虛擬機(jī)內(nèi)部引用,比如Integer這些基本類型的
2.3再談引用
- 強(qiáng)引用:垃圾回收器不會對他進(jìn)行回收
- 軟引用:內(nèi)存空間不足會回收
- 弱引用:gc就回收
- 虛引用:隨時會被回收而且需要引用隊列
虛引用、軟引用、弱引用的區(qū)別?
- 虛引用的對象在gc之前會被送到引用隊列,并且程序在對象回收之前做相應(yīng)的活動(臨死之前的處理)
- 軟引用是用的最多的,可以提高gc的效率,維護(hù)系統(tǒng)安全,防止內(nèi)存溢出
2.4不可達(dá)對象不一定回收
- 在回收之前會對對象進(jìn)行一次標(biāo)記,看是否會執(zhí)行finalize方法。如果沒有那么這些對象將會先被回收
- 如果有那么進(jìn)行第二次標(biāo)記,讓對象執(zhí)行finalize之后再進(jìn)行回收
2.5如何判斷一個常量是廢棄常量?
- 如果常量池對象沒有被任何對象引用就會被回收
- jdk1.7之前運(yùn)行時常量池包含字符串常量池,需要進(jìn)行復(fù)制來返回新的引用(堆有一個,常量池有一個)
- jdk1.7的時候字符串池已經(jīng)不在運(yùn)行時常量池,如果調(diào)用intern就會把當(dāng)前對象放入常量池并且返回引用(只有常量池有一個)。如果本來就存在就會返回對象實(shí)例的地址。
- jdk1.8之后運(yùn)行時常量池已經(jīng)轉(zhuǎn)移到了元空間
2.6如果判斷一個類沒有用?
- 類的實(shí)例都回收了
- 類的類加載器回收了
- 類信息沒有被引用
- 大量的反射和動態(tài)代理生成類信息會對方法區(qū)產(chǎn)生很大的壓力
3.垃圾回收算法
hotspot為什么要區(qū)分老年代和新生代?
原因就是不同的存活對象需要不同的垃圾回收算法
- 如果新生代用的是標(biāo)記整理,問題就是每次清除大量的對象,移動時間很長,整理消耗很大。但是標(biāo)記復(fù)制就很快,因為存活對象少
- 但是老年代如果使用標(biāo)記整理就很好,因為存活多移動少,復(fù)制就相反
- 不能夠統(tǒng)一設(shè)計為弱分代假說和強(qiáng)分代假說
跨代收集假說?
如果老年代和新生代互相引用,新生代的年齡就會被拉長。但是為了知道新生代什么時候被gc,這個時候可以給新生代加上一個記憶集(把老年代劃分為很多個格子,代表誰引用了我),避免掃描整個老年代
4.垃圾回收器
4.1Serial收集器
- 單線程收集器,每次都要阻塞其它線程(STW),一個垃圾線程單獨(dú)回收
- 新生代是標(biāo)記復(fù)制,老年代是標(biāo)記整理
- 它簡單高效,沒有和其它線程交換不會產(chǎn)生并發(fā)問題
- 但是STW會導(dǎo)致響應(yīng)很慢
4.2ParNew收集器
- Serial的多線程版本,但是還是會STW
- 新生代是標(biāo)記復(fù)制,老年代是標(biāo)記整理
4.3Parallel Scavenge收集器
- 新生代是標(biāo)記復(fù)制,老年代是標(biāo)記整理
- 和ParNew不同的地方就是它完全關(guān)注cpu的利用率,也就是處理任務(wù)的吞吐量,而不會管STW到底停多久
4.4SerialOld
- Serial的老年代版本,1.5以前和Parallel Scavenge一起使用,還有別的用途就是CMS的后備方案
4.5Parallel Old收集器
- Parallel Scavenge收集器的老年代也是注重吞吐量
4.6CMS收集器
- 注重最小響應(yīng)時間
- 垃圾收集器和用戶線程同時工作
- 初始標(biāo)記記錄gc root直接相連的對象
- 并發(fā)標(biāo)記遍歷整個鏈,但是可以和用戶線程并發(fā)運(yùn)行
- 重新標(biāo)記修正那些更新的對象的引用鏈,比并發(fā)標(biāo)記短
- 并發(fā)清除
問題?
內(nèi)存碎片多對cpu資源敏感
4.7G1收集器
同時滿足響應(yīng)快處理多的問題
特點(diǎn)
- 并行和并發(fā),使用多個cpu執(zhí)行g(shù)c線程來縮短stw,而且還能與java線程并發(fā)執(zhí)行
- 分代收集
- 空間整合:大部分時候使用標(biāo)記復(fù)制
- 可預(yù)測停頓:響應(yīng)時間快,可以設(shè)置stw時間
- 分區(qū)之間的跨代引用,young這里使用了rset(非收集區(qū)指向收集區(qū))記錄,老年代那個區(qū)域指向了我,老年代使用了卡表劃分了很多個區(qū)域,那么minor gc的時候就不需要遍歷整個其它所有區(qū)域去看看當(dāng)前的區(qū)域的對象到底有沒有被引用。
補(bǔ)充字符串池的本質(zhì)
第一個問題是String a="a"的時候做了什么?
- 先去找常量池是否存在a如果存在那么就直接返回常量池的引用地址返回,如果不存在那么就創(chuàng)建一個在常量池然后再返回引用地址
第二個問題new String(“a”)發(fā)生了什么?
- 先看看常量池是否存在a,如果不存在創(chuàng)建一個在常量池,而且在堆單獨(dú)創(chuàng)建一個a對象返回引用(而不是返回常量池的),相當(dāng)于就是創(chuàng)建了兩次。
- 如果第二次創(chuàng)建發(fā)現(xiàn)已經(jīng)存在就直接在堆中創(chuàng)建對象。
第三個問題intern的原理?
- 看看常量池有沒有這個字符串,沒有就創(chuàng)建并返回常量池對象的地址引用
- 如果有那么直接返回常量池對象的地址引用
String s1=new String(“a”)
String s2=s1.intern();
很明顯s1不等于s2如果上面的問題都清晰知道。s1引用的是堆,而s2引用的是常量池的
第四個問題
String s3=new String(“1”)+new String(“1”);
String s5=s3.intern();
String s4=“11”
那么地方他們相等嗎?當(dāng)然是相等的,s3會把1存入常量池,但是不會吧11存入常量池因為,還沒編譯出來。調(diào)用了intern之后才會把對象存入常量池,而這個時候存入的對象就是s3指向的那個。所以s4指向的也是s3的。如果是s0="11"的話那就不一樣了,s3.intern只會返回常量池的對象引用地址,而不是s3的,因為s3是不能重復(fù)intern 11進(jìn)去的。jdk1.6的話那么無論怎么樣都是錯的,intern是復(fù)制一份,而不是把對象存入常量池(因為字符串常量池在方法區(qū),而jdk1.7它在堆所以可以很好的保存s3的引用)
下面的代碼正確分析應(yīng)該是三個true,但是在test里面就會先緩存了11導(dǎo)致false, true,false的問題。
@Test public void test4(){ String s3 = new String("1") + new String("1"); String s5 = s3.intern(); String s4 = "11"; System.out.println(s5 == s3); System.out.println(s5 == s4); System.out.println(s3 == s4); System.out.println("======================"); String s6 = new String("go") +new String("od"); String s7 = s6.intern(); String s8 = "good"; System.out.println(s6 == s7); System.out.println(s7 == s8); System.out.println(s6 == s8); }
finalize的原理
- 其實(shí)就是對象重寫了finalize,那么第一次gc的時候如果發(fā)現(xiàn)有finalize,就會把對象帶到F-Queue上面等待,執(zhí)行finalize方法進(jìn)行自救,下面就是一個自救過程,new了一個GCTest對象,這個時候test不引用了,那么正常來說這個GCTest就會被回收,但是它觸發(fā)了finalize的方法,最后再次在finalize中使用test引用它所以對象沒有被消除
- 但是finalize是一個守護(hù)線程,防止有的finalize是個循環(huán)等待方法阻塞整個隊列,影響回收效率
- 最后一次標(biāo)記就是在F-queue里面標(biāo)記這個對象(如果沒有引用)然后釋放
- finalize實(shí)際上是放到了Finalizer線程上實(shí)現(xiàn)。然后然引用隊列指向這個雙向鏈表,一旦遇到gc,那么就會調(diào)用ReferenceHandler來處理這些節(jié)點(diǎn)的finalize調(diào)用,調(diào)用之后斷開節(jié)點(diǎn),節(jié)點(diǎn)就會被回收了
- finalize上鎖導(dǎo)致執(zhí)行很慢
public class GCTest { static GCTest test; public void isAlive(){ System.out.println("我還活著"); } @Override protected void finalize() throws Throwable { System.out.println("我要死了"); test=this; } public static void main(String[] args) throws InterruptedException { test = new GCTest(); test=null; System.gc(); Thread.sleep(500); if(test!=null){ test.isAlive(); }else{ System.out.println("死了"); } test=null; System.gc(); if(test!=null){ test.isAlive(); }else{ System.out.println("死了"); } } }
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注服務(wù)器之家的更多內(nèi)容!
原文鏈接:https://blog.csdn.net/m0_46388866/article/details/120916881