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