轉(zhuǎn)載自 http://ginobefunny.com/post/jvm_interview_questions/
Java內(nèi)存區(qū)域是如何劃分的?
- Java堆:線程共享的,唯一目的就是用于存放對(duì)象實(shí)例,是垃圾收集器管理的主要區(qū)域;
- Java虛擬機(jī)棧:線程私有的,每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量等,局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型和對(duì)象引用;
- 本地方法棧:和虛擬機(jī)棧類似,不過它是為Native方法服務(wù);
- 程序計(jì)數(shù)器:線程私有的,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,以便線程切換后恢復(fù)執(zhí)行使用;
- 方法區(qū):線程共享的,用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù);該區(qū)域的內(nèi)存回收主要是針對(duì)常量池的回收和類型的卸載(特別是要注意一些動(dòng)態(tài)字節(jié)碼框架和自定義ClassLoader的場(chǎng)景下);在HotSpot里經(jīng)常被稱為永久代,在Java 8里已被廢除了,被元空間取代;
對(duì)象是否可用以及引用類型。
- 由于引用計(jì)數(shù)法無法解決循環(huán)引用的問題,所以一般都是使用可達(dá)性分析來判斷的,即通過一系列稱為“GC Roots”的對(duì)象(比如虛擬機(jī)棧引用的對(duì)象、方法區(qū)中的類靜態(tài)屬性和常量引用對(duì)象)作為起點(diǎn),從這些節(jié)點(diǎn)一直往下搜索,走過的路徑稱為引用鏈;而那些沒有與引用鏈相連的對(duì)象即為不可達(dá),會(huì)被回收;
- 可以通過覆蓋finalize方法來實(shí)現(xiàn)對(duì)象的“自救”,避免在標(biāo)記后被回收,但通常不建議這么做;
- 對(duì)象的引用類型可分為:強(qiáng)引用、軟引用(在內(nèi)存溢出前會(huì)將這種類型的對(duì)象進(jìn)行第二次回收)、弱引用(弱引用對(duì)象只能生存到下次垃圾回收之前)、虛引用(不會(huì)對(duì)生存時(shí)間存在影響,也無法通過它獲取對(duì)象,主要目的就是在回收時(shí)收到一個(gè)系統(tǒng)通知);
有哪些常見的垃圾收集算法?
- 標(biāo)記-清除算法:首先標(biāo)記出所有需要回收的對(duì)象,然后統(tǒng)一回收所有被標(biāo)記的對(duì)象;缺點(diǎn)是效率不高且容易產(chǎn)生大量不連續(xù)的內(nèi)存碎片;
- 復(fù)制算法:將可用內(nèi)存分為大小相等的兩塊,每次只使用其中一塊;當(dāng)這一塊用完了,就將還活著的對(duì)象復(fù)制到另一塊上,然后把已使用過的內(nèi)存清理掉。在HotSpot里,考慮到大部分對(duì)象存活時(shí)間很短,將內(nèi)存分為Eden和兩塊Survivor,默認(rèn)比例為8:1:1。代價(jià)是存在部分內(nèi)存空間浪費(fèi),且可能存在空間不夠需要分配擔(dān)保的情況,所以適合在新生代使用;
- 標(biāo)記-整理算法:首先標(biāo)記出所有需要回收的對(duì)象,然后讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。適用于老年代。
- 分代收集算法:一般把Java堆分新生代和老年代,在新生代用復(fù)制算法,在老年代用標(biāo)記-清理或標(biāo)記-整理算法,是現(xiàn)代虛擬機(jī)通常采用的算法。
PS:堆的劃分及回收過程詳解
1\. Eden區(qū)最大,對(duì)外提供堆內(nèi)存。當(dāng)Eden區(qū)快要滿了,則進(jìn)行Minor GC,把存活對(duì)象放入Survivor A區(qū),清空Eden區(qū);
2\. Eden區(qū)被清空后,繼續(xù)對(duì)外提供堆內(nèi)存;
3\. 當(dāng)Eden區(qū)再次被填滿,此時(shí)對(duì)Eden區(qū)和Survivor A區(qū)同時(shí)進(jìn)行Minor GC,把存活對(duì)象放入Survivor B區(qū),同時(shí)清空Eden 區(qū)和Survivor A區(qū);
4\. Eden區(qū)繼續(xù)對(duì)外提供堆內(nèi)存,并重復(fù)上述過程,即在Eden區(qū)填滿后,把Eden區(qū)和某個(gè)Survivor區(qū)的存活對(duì)象放到另一個(gè)Survivor區(qū);
5\. 當(dāng)某個(gè)Survivor區(qū)被填滿,且仍有對(duì)象未被復(fù)制完畢時(shí)或者某些對(duì)象在反復(fù)Survive 15次左右時(shí),則把這部分剩余對(duì)象放到Old區(qū);
6\. 當(dāng)Old區(qū)也被填滿時(shí),進(jìn)行Major GC,對(duì)Old區(qū)進(jìn)行垃圾回收。
有哪些常見的垃圾收集器?
這里討論JDK 1.7 Update 14之后的HotSpot虛擬機(jī),包含的虛擬機(jī)如下圖所示(存在連線的表示可以搭配使用):
Serial收集器
- 最基本、發(fā)展歷史最悠久,在JDK 1.3之前是新生代收集的唯一選擇;
- 是一個(gè)單線程(只會(huì)使用一個(gè)收集線程,且必須暫停所有工作線程)的收集器,采用的是復(fù)制算法;
- 現(xiàn)在依然是虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)新生代收集器,主要就是因?yàn)樗?jiǎn)單而高效(沒有線程交互的開銷);
ParNew收集器
- 其實(shí)就是Serial收集器的多線程版本,采用的也是復(fù)制算法;
- ParNew收集器在單CPU環(huán)境中絕對(duì)不會(huì)有比Serial收集器更好的效果;
- 是許多運(yùn)行在Server模式下虛擬機(jī)首選的新生代收集器,重要原因就是除了Serial收集器外,只有它能與CMS收集器配合工作;
PS:關(guān)于垃圾收集器的并行和并發(fā)
- 并行(Parallel):指多條垃圾收集線程并行工作,但此時(shí)用戶線程仍處于等待狀態(tài);
- 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時(shí)執(zhí)行,用戶線程在繼續(xù)執(zhí)行而垃圾收集程序運(yùn)行在另外一個(gè)CPU上;
CMS收集器
- 是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,特別適合互聯(lián)網(wǎng)站或者B/S的服務(wù)端;
- 它是基于標(biāo)記-清除 算法實(shí)現(xiàn)的,主要包括4個(gè)步驟:初始標(biāo)記(STW,只是初始標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快)、并發(fā)標(biāo)記(非STW,執(zhí)行GC RootsTracing,耗時(shí)比較長(zhǎng))、重新標(biāo)記(STW,修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)導(dǎo)致變動(dòng)的那一部分對(duì)象標(biāo)記)和并發(fā)清除(非STW,耗時(shí)較長(zhǎng));
- 還有3個(gè)明顯的缺點(diǎn):CMS收集器對(duì)CPU非常敏感(占用部分線程及CPU資源,影響總吞吐量)、無法處理浮動(dòng)垃圾(默認(rèn)達(dá)到92%就觸發(fā)垃圾回收)、大量?jī)?nèi)存碎片產(chǎn)生(可以通過參數(shù)啟動(dòng)壓縮);
介紹一下G1收集器的原理和實(shí)現(xiàn)。
- 一款面向服務(wù)端應(yīng)用的垃圾收集器,后續(xù)會(huì)替換掉CMS垃圾收集器;
- 特點(diǎn):
并行與并發(fā)(充分利用多核多CPU縮短STW時(shí)間)
分代收集(獨(dú)立管理整個(gè)Java堆,但針對(duì)不同年齡的對(duì)象采取不同的策略)
空間整合(局部看是基于復(fù)制算法,從整體來看是基于標(biāo)記-整理算法,都不會(huì)產(chǎn)生內(nèi)存碎片)
可預(yù)測(cè)的停頓(可以明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片內(nèi)垃圾收集不會(huì)超過N毫秒)
- 將堆分為大小相等的獨(dú)立區(qū)域,避免全區(qū)域的垃圾收集;新生代和老年代不再物理隔離,只是部分Region的集合;
- G1跟蹤各個(gè)Region垃圾堆積的價(jià)值大小,在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,根據(jù)允許的收集時(shí)間優(yōu)先回收價(jià)值最大的Region;
- Region之間的對(duì)象引用以及其他收集器中的新生代與老年代之間的對(duì)象引用,采用Remembered Set來避免全堆掃描;
- 分為幾個(gè)步驟,和CMS的過程比較類似:
初始標(biāo)記(標(biāo)記一下GC Roots能直接關(guān)聯(lián)的對(duì)象并修改TAMS值,需要STW但耗時(shí)很短)
并發(fā)標(biāo)記(從GC Root從堆中對(duì)象進(jìn)行可達(dá)性分析找存活的對(duì)象,耗時(shí)較長(zhǎng)但可以與用戶線程并發(fā)執(zhí)行)
最終標(biāo)記(為了修正并發(fā)標(biāo)記期間產(chǎn)生變動(dòng)的那一部分標(biāo)記記錄,這一期間的變化記錄在Remembered
Set Log里,然后合并到Remembered Set里,該階段需要STW但是可并行執(zhí)行)
篩選回收(對(duì)各個(gè)Region回收價(jià)值排序,根據(jù)用戶期望的GC停頓時(shí)間制定回收計(jì)劃來回收);
你們的服務(wù)配置的虛擬機(jī)參數(shù)是怎么樣的?
我們的服務(wù)的虛擬機(jī)參數(shù):
-server --啟用能夠執(zhí)行優(yōu)化的編譯器,顯著提高服務(wù)器的性能
-Xmx4000M --堆最大值
-Xms4000M --堆初始大小
-Xmn600M --年輕代大小
-XX:PermSize=200M --持久代初始大小
-XX:MaxPermSize=200M --持久代最大值
-Xss256K --每個(gè)線程的棧大小
-XX:+DisableExplicitGC --關(guān)閉System.gc()
-XX:SurvivorRatio=1 --年輕代中Eden區(qū)與兩個(gè)Survivor區(qū)的比值
-XX:+UseConcMarkSweepGC --使用CMS內(nèi)存收集
-XX:+UseParNewGC --設(shè)置年輕代為并行收集
-XX:+CMSParallelRemarkEnabled --降低標(biāo)記停頓
-XX:+UseCMSCompactAtFullCollection --在FULL GC的時(shí)候,對(duì)年老代進(jìn)行壓縮,可能會(huì)影響性能,但是可以消除碎片
-XX:CMSFullGCsBeforeCompaction=0 --此值設(shè)置運(yùn)行多少次GC以后對(duì)內(nèi)存空間進(jìn)行壓縮、整理
-XX:+CMSClassUnloadingEnabled --回收動(dòng)態(tài)生成的代理類 SEE:http://stackoverflow.com/questions/3334911/what-does-jvm-flag-cmsclassunloadingenabled-actually-do
-XX:LargePageSizeInBytes=128M --內(nèi)存頁(yè)的大小不可設(shè)置過大, 會(huì)影響Perm的大小
-XX:+UseFastAccessorMethods --原始類型的快速優(yōu)化
-XX:+UseCMSInitiatingOccupancyOnly --使用手動(dòng)定義初始化定義開始CMS收集,禁止hostspot自行觸發(fā)CMS GC
-XX:CMSInitiatingOccupancyFraction=80 --使用cms作為垃圾回收,使用80%后開始CMS收集
-XX:SoftRefLRUPolicyMSPerMB=0 --每兆堆空閑空間中SoftReference的存活時(shí)間
-XX:+PrintGCDetails --輸出GC日志詳情信息
-XX:+PrintGCApplicationStoppedTime --輸出垃圾回收期間程序暫停的時(shí)間
-Xloggc:$WEB_APP_HOME/.tomcat/logs/gc.log --把相關(guān)日志信息記錄到文件以便分析.
-XX:+HeapDumpOnOutOfMemoryError --發(fā)生內(nèi)存溢出時(shí)生成heapdump文件
-XX:HeapDumpPath=$WEB_APP_HOME/.tomcat/logs/heapdump.hprof --heapdump文件地址
如何進(jìn)行性能調(diào)優(yōu)以及常用的JDK的命令行工具有哪些?
- JVM調(diào)優(yōu):CPU使用率與Load值偏大(Thread count以及GC count)、關(guān)鍵接口響應(yīng)時(shí)間很慢(GC time以及GC log中的STW的時(shí)間)、發(fā)生Full GC或者Old CMS GC非常頻繁(內(nèi)存泄露);
- JVM停頓(盡量避免Full GC、關(guān)閉偏向鎖、輸出GC日志到內(nèi)存文件系統(tǒng)、關(guān)閉JVM輸出的jstat日志);
- 將Java性能優(yōu)化分為4個(gè)層級(jí):應(yīng)用層、數(shù)據(jù)庫(kù)層、框架層、JVM層。每層優(yōu)化難度逐級(jí)增加,涉及的知識(shí)和解決的問題也會(huì)不同。比如應(yīng)用層需要理解代碼邏輯,通過Java線程棧定位有問題代碼行等;數(shù)據(jù)庫(kù)層面需要分析SQL、定位死鎖等;框架層需要懂源代碼,理解框架機(jī)制;JVM 層需要對(duì)GC的類型和工作機(jī)制有深入了解,對(duì)各種 JVM 參數(shù)作用了然于胸;
- 圍繞Java性能優(yōu)化,有兩種最基本的分析方法:現(xiàn)場(chǎng)分析法和事后分析法?,F(xiàn)場(chǎng)分析法通過保留現(xiàn)場(chǎng),再采用診斷工具分析定位?,F(xiàn)場(chǎng)分析對(duì)線上影響較大,部分場(chǎng)景不太合適。事后分析法需要盡可能多收集現(xiàn)場(chǎng)數(shù)據(jù),然后立即恢復(fù)服務(wù),同時(shí)針對(duì)收集的現(xiàn)場(chǎng)數(shù)據(jù)進(jìn)行事后分析和復(fù)現(xiàn)。
- OS 的診斷主要關(guān)注的是 CPU、Memory、I/O 三個(gè)方面。top、vmstat、 free –m、iostat;常用的Java應(yīng)用診斷包括線程、堆棧、GC 等方面的診斷,可以使用jstack 、jstat、jmap;
類的加載器是什么?
- 虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把類加載階段的“通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到虛擬機(jī)外部去實(shí)現(xiàn),實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為類加載器;這種設(shè)計(jì)給Java語言帶來了非常強(qiáng)大的靈活性;
- 雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其他的類加載器都應(yīng)當(dāng)有自己的父類加載器,如果一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,只有父類加載器反饋?zhàn)约簾o法完成這個(gè)加載請(qǐng)求時(shí),子加載器才會(huì)嘗試自己去加載;這對(duì)于保證程序的穩(wěn)定運(yùn)作很重要;
- OSGI實(shí)現(xiàn)模塊化熱部署的關(guān)鍵是它自定義的類加載機(jī)制的實(shí)現(xiàn),每個(gè)Bundle(通過Import-Package和Export-Package導(dǎo)入和導(dǎo)出依賴)都有自己的類加載器,類加載器之間形成了更加復(fù)雜的網(wǎng)狀結(jié)構(gòu);
談?wù)勀銓?duì)Java內(nèi)存模型的理解。
- 虛擬機(jī)規(guī)范視圖通過JMM來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,主要目標(biāo)是定義程序中各個(gè)變量的訪問限制,即在虛擬機(jī)將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié);
- 主內(nèi)存與工作內(nèi)存:Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,每個(gè)線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量;
- 可見性(主內(nèi)存和工作內(nèi)存)、原子性(volatile的long是具備原子性的)、有序性(happen—before規(guī)則);
- Java語言中有一個(gè)“先行發(fā)生”(happen—before)的規(guī)則,它是Java內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果操作A先行發(fā)生于操作B,其意思就是說,在發(fā)生操作B之前,操作A產(chǎn)生的影響都能被操作B觀察到,“影響”包括修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等,它與時(shí)間上的先后發(fā)生基本沒有太大關(guān)系。下面是Java內(nèi)存模型中的八條可保證happen—before的規(guī)則,它們無需任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個(gè)操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來的話,它們就沒有順序性保障,虛擬機(jī)可以對(duì)它們進(jìn)行隨機(jī)地重排序。
1、程序次序規(guī)則:在一個(gè)單獨(dú)的線程中,按照程序代碼的執(zhí)行流順序,(時(shí)間上)先執(zhí)行的操作happen—before(時(shí)間上)后執(zhí)行的操作。
2、管理鎖定規(guī)則:一個(gè)unlock操作happen—before后面(時(shí)間上的先后順序,下同)對(duì)同一個(gè)鎖的lock操作。
3、volatile變量規(guī)則:對(duì)一個(gè)volatile變量的寫操作happen—before后面對(duì)該變量的讀操作。
4、線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法happen—before此線程的每一個(gè)動(dòng)作。
5、線程終止規(guī)則:線程的所有操作都happen—before對(duì)此線程的終止檢測(cè),可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測(cè)到線程已經(jīng)終止執(zhí)行。
6、線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用happen—before發(fā)生于被中斷線程的代碼檢測(cè)到中斷時(shí)事件的發(fā)生。
7、對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)happen—before它的finalize()方法的開始。
8、傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
(http://ginobefunny.com/post/jvm_interview_questions/#%E6%98%AF%E5%90%A6%E4%BA%86%E8%A7%A3%E5%81%8F%E5%90%91%E9%94%81%EF%BC%9F "是否了解偏向鎖?")是否了解偏向鎖?
JVM鎖有4種狀態(tài):無鎖、偏向鎖(通過MarkWord的線程ID)、輕量級(jí)鎖(通過MarkWord的鎖記錄指針)、重量級(jí)鎖;