JVM必知必會

1.內存模型以及分區,須要詳細到每一個區放什麼?

虛擬機將所管理的內存分爲如下幾個部分:html

  • 程序計數器 記錄的是正在執行的虛擬機字節碼指令的地址,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成,是當前線程所執行的字節碼的行號指示器,Java虛擬機的多線程其實是線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令,所以爲了線程切換後能恢復到正確的執行位置,每條線程處理器都須要有一個獨立的程序計數器,互不影響,獨立存儲線程私有的內存,且是JVM中惟一沒有規定OOM狀況的區域。
  • 虛擬機棧 線程私有、與線程生命週期相同描述Java方法執行的內存模型,存放局部變量表、操做數棧、動態連接、方法出口等信息,咱們常說的"棧"即指虛擬機棧或者說其中的局部變量表部分。局部變量表中存放:編譯期可知的各類基本數據類型、對象引用類型等。若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常,若是虛擬機棧擴展時沒法申請到足夠的內存,將會拋出OOM異常。
  • 本地方法棧 與虛擬機棧做用很是類似,虛擬機棧爲Java方法服務,本地方法棧爲Native方法服務。
  • Java堆 所管理內存中最大的一塊,線程共享,惟一目的是存放對象實例,垃圾收集器管理的主要區域,若是在堆中沒有內存完成實例分配,而且堆再也沒法擴展時,將會拋出OutOfMemoryError異常。
  • 方法區 (非堆) 線程共享,用於存儲已被虛擬機加載的類信息、常量、靜態變量、及時編譯器編譯的代碼等數據。對於HotSpot虛擬機方法區被稱爲:永久代(Permanent Generation),緣由是使用永久代實現方法區而已。
  • 運行時常量池  方法區一部分,存放編譯期生成的各類字面量和符號引用。字面量(Literal)和符號引用量(Symbolic References),字面量至關於Java語言層面常量的概念,如文本字符串,聲明爲final的常量值等,符號引用則屬於編譯原理方面的概念,包括了以下三種類型的常量:類和接口的全限定名,字段名稱和描述符,方法名稱和描述符。運行時常量池在JDK1.6及以前版本的JVM中是方法區的一部分,而在HotSpot虛擬機中方法區放在了」永久代(Permanent Generation)」。因此運行時常量池也是在永久代的。 

在Java8中,永久代已經被移除,被一個稱爲「元數據區」(Metaspace)的區域所取代。元空間的本質和永久代相似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。java

  • 直接內存(堆外內存機器內存中,不屬於堆內存的部分即爲堆外內存。堆外內存會溢出,而且其垃圾回收依賴於代碼顯式調用System.gc()。考慮使用緩存時,本地緩存是最快速的,但會給虛擬機帶來GC壓力,使用硬盤或者分佈式緩存的響應時間會比較長,這時候堆外緩存會是一個比較好的選擇。存在兩種分配堆外內存的方法,Unsafe和NIO ByteBuffer,也可使用Ehcache,Ehcache支持堆內緩存、堆外緩存、磁盤緩存、分佈式緩存。

2.堆裏面的分區:Eden、From Survivor、To Survivor

在 Java 中,堆被劃分紅兩個不一樣的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分爲三個區域:Eden、From Survivor、To Survivor。 程序員

(1)大多狀況下,對象在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC;(2)大對象(須要大量連續內存空間的對象)直接進入老年代;(3)長期存活對象將進入老年代。算法

Minor GC(新生代GC):在新生代的垃圾收集動做,比較頻繁,回收速度較快,採用複製算法。數組

Full GC(老年代GC,Major GC):發生在老年代的GC,速度較慢,採用標記-清除算法等。緩存

回收過程:多線程

當對象在 Eden ( 包括一個 Survivor 區域,這裏假設是 from 區域 ) 出生後,在通過一次 Minor GC 後,若是對象還存活,而且可以被另一塊 Survivor 區域所容納(上面已經假設爲 from 區域,這裏應爲 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用複製算法將這些仍然還存活的對象複製到另一塊 Survivor 區域 ( 即 to 區域 ) 中,而後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),而且將這些對象的年齡設置爲1,之後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,能夠經過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成爲老年代。併發

注:有的資料還有持久代的概念,存儲類信息、常量、靜態變量、類方法。app

3.內存分配與回收策略

自動內存管理解決的主要問題:給對象分配內存以及回收分配給對象的內存。內存分配規則並不是徹底固定,取決於當前使用的垃圾回收器組合還有虛擬機內存參數設定。如下是廣泛的內存分配規則:分佈式

  • 對象優先在Eden分配,若是Eden沒有足夠空間分配時,將發起一次Minor GC。
  • 大對象直接進入老年代,大對象是指須要大量連續內存空間的Java對象,如很長的字符串和數組。
  • 長期存活的對象將進入老年代,對象年齡計數器判斷,爲適應不一樣程序的運行情況,若是在Survivor空間相同年齡全部對象大小的總和大於Survivor的一半,年齡大於或等於該年齡的對象能夠直接進入老年代。
  • 空間分配擔保,若是Minor GC存活後的對象突增,遠遠高於平均值的話,將會致使老年代內存分配擔保失敗,此時會觸發Full GC(具體能夠根據參數設置是否容許冒險,若是容許,則根據平均值進行Minor GC嘗試,若是不容許冒險則會進行一次Full GC)。

4.對象的建立方法,對象的內存分配,對象的訪問定位

對象的建立

(1)檢查常量池是否能夠定位到一個類的符號引用,檢查是否被加載、解析和初始化過

(2)加載檢查經過後,爲新生對象分配內存(內存分配方式:指針碰撞、空閒列表)

(3)內存分配完成後,初始化零值

(4)對對象進行必要的設置,如對象是哪一個類的實例、元數據信息、對象的哈希碼、

GC分代信息,這些信息存放在對象頭中。

(5)執行<init>方法,把對象按照程序員的意願進行初始化。

對象的內存佈局

三塊區域:對象頭(對象自身運行時數據;類型指針->對象指向它的類元數據的指針,用於肯定該對象屬於哪一個類的實例)、實例數據(對象真正存儲的有效信息)、對齊填充(起到佔位符的做用)。

對象的訪問定位

經過棧上的reference數據來操做堆上的具體對象。主流的訪問方式有:使用句柄、直接指針。

(1)使用句柄:劃份內存做爲句柄池,reference中存儲對象的句柄地址,最大的好處是存儲的句柄地址是穩定的,在對象移動時reference自己不須要修改。

(2)使用直接指針:reference中存儲的直接就是對象對象地址,速度更快,節省了一次指針定位的時間開銷。HotSpot採用。

5.GC的兩種斷定方法(對象已死嗎)

引用計數法:經過引用計數器實現,任什麼時候刻計數器爲0的對象就是不可能再被使用的。實現簡單,斷定效率很高,但很難解決對象之間的互相循環引用問題。

可達性分析算法:經過一系列的稱爲「GC ROOTS」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC ROOTS沒有任何引用鏈相連時(圖論的觀點,就是從GC ROOTS到這個對象不可達),則證實對象是不可用的。

6.GC的垃圾收集算法

標記-清除算法:分爲兩個階段,首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。存在的兩個問題:效率問題;產生大量不連續的內存碎片。

複製算法:每次只使用其中一塊,當一塊內存用完了,就將還存活的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。在存活對象很少的狀況下,性能高,能解決內存碎片問題。經常使用於回收新生代。萬一存活對象數量比較多,那麼To域的內存可能不夠存放,這個時候會藉助老年代的空間,而老年代因爲沒有其額外內存空間進行分配擔保,通常不採用複製算法。

標記-整理算法:標記過程與標記-清除算法同樣,但後續步驟不是直接對可回收對象進行整清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。

分代收集算法:根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率較高、沒有額外空間對它進行分配擔保,就必須使用後面兩種算法來進行回收。

7.GC收集器有哪些?

Serial垃圾收集器(單線程、複製算法):Serial 是一個單線程的收集器,它不但只會使用一個CPU或一條線程去完成垃圾收集工 做,而且在進行垃圾收集的同時,必須暫停其餘全部的工做線程,直到垃圾收集結束。是java 虛擬機運行在Client模式下默認的新生代垃圾收集器。

ParNew垃圾收集器(Serial+多線程),Serial收集器的多線程版本,使用複製算法,是不少java虛擬機運行在Server模式下新生代的默認垃圾收集器。

Parallel Scavenge 收集器(多線程複製算法、高效)

SerialOld收集器(單線程標記整理算法)

ParallelOld收集器(多線程標記整理算法)

CMS收集器(多線程標記清除算法:一種以獲取最短回收停頓時間爲目標的收集器,在重視服務的響應速度、注重用戶體驗的場景下很是適用。基於「標記-清除」算法,特色是:併發收集、低停頓。具體分爲四個階段:初始標記->併發標記->從新標記->併發清除。

初始標記和從新標記兩個步驟須要STW,sun官方文檔稱做併發低停頓收集器,主要有如下侷限性:(1)對CPU資源很是敏感,在併發階段,雖然不會致使用戶線程停頓,可是會佔用一部分線程或者說CPU資源致使應用程序變慢,總吞吐量下降,默認啓動的回收線程數是(CPU數量+3)/4。(2)沒法處理浮動垃圾,運行期間預留的內存沒法知足程序須要,出現Concurrent Mode Fail, 可能致使另外一次Full GC的發發生。(3)內存碎片,能夠犧牲停頓時間開啓內存碎片的合併整理過程。

G1收集器:面向服務端應用的垃圾收集器,可能代替CMS收集器,特色:並行與併發分代收集(能夠不須要其餘收集器配合就能獨立管理整個堆,採用不一樣的方式分代收集),空間整合(總體上是「標記-整理」算法實現,局部上是「複製」算法實現,不會產生內存空間碎片),能夠預測的停頓時間(創建可預測的停頓時間模型)。具體能夠分爲四個階段:初始標記->併發標記->最終標記->篩選回收。

引入分區的思路,弱化了分代的概念,使用G1垃圾收集器時,Java堆的內存佈局與其餘收集器有很大的差異,它將整個堆劃分對多個大小相等的獨立區域(region),新生代、老年代的概念依然保留,再也不是物理隔離而是一部分region(不須要連續)的集合,回收時則以分區爲單位進行回收。

G1的設計原則是"首先收集儘量多的垃圾"。所以,G1並不會等內存耗盡(串行、並行)或者快耗盡(CMS)的時候開始垃圾收集,而是使用啓發式算法,跟蹤各個region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,優先回收價值最大的region(這是garbage first名字的由來)。同時G1能夠根據用戶設置的暫停時間目標自動調全年輕代和總堆大小。

G1的收集都是STW的,但年輕代和老年代的收集界限比較模糊,採用了混合(mixed)收集的方式。即每次收集既可能只收集年輕代分區(年輕代收集),也可能在收集年輕代的同時,包含部分老年代分區(混合收集),這樣即便堆內存很大時,也能夠限制收集範圍,從而下降停頓。

8.Minor GC與Full GC分別在何時發生?

Minor GC(新生代GC):在新生代的垃圾收集動做,比較頻繁,回收速度較快,採用複製算法。

Full GC(老年代GC,Major GC):發生在老年代的GC,速度較慢,採用標記-清除算法等。

9. 幾種經常使用的內存調試工具:jmap、jstack、jconsole

jmap:用於生成堆轉儲快照(heap dump或dump文件),還能夠查詢finalize隊列,java堆和永久代的詳細信息,如空間使用率、當前使用的收集器等。

jstack:java堆棧跟蹤工具,生成虛擬機當前時刻的線程快照,主要目的在於定位線程長時間停頓的緣由,如出現死鎖、死循環、請求外部資源致使的長時間等待。線程出現停頓的時候經過jstack查看各個線程的調用堆棧,就能夠知道沒有響應的線程到底在後臺作什麼或者等待什麼資源。

jconsole:可視化監視、管理工具,內存監控(至關於可視化的jstat命令,用於監視受收集器管理的虛擬機內存的變化趨勢),線程監控(至關於可視化得jstack命令,遇到線程停頓時能夠進行監控分析)

10.類加載的五個過程:加載、驗證、準備、解析、初始化

類加載或者初始化的三個步驟:加載、鏈接、初始化

(1)加載:將類的class文件讀入內存,併爲之建立一個java.lang.Class對象,類的加載由類加載器完成。

(2)鏈接:鏈接階段負責把類的二進制數據合併到JRE中,具體又分爲以下三個階段:驗證,驗證階段用於檢驗被加載的類是否有正確的內部結構,並和其餘類協調一致;

準備,類準備階段負責爲類的類變量分配內存,並設置默認初始值。

解析,將類的二進制數據中的符號引用替換爲直接引用

(3)初始化:虛擬機負責對類初始化,主要就是對類變量進行初始化。假設這個類尚未被加載和鏈接,則程序先加載並鏈接這個類,假設該類的直接父類尚未被初始化,則先初始化其直接父類,假設類中有初始化語句,則系統依次執行這些初始化語句。

11.雙親委派模型

BootstrapClassLoader(根類加載器)、ExtensionClassLoader(擴展類加載器)、ApplicationClassLoader(應用程序類加載器或者叫系統類加載器)

(1)根類加載器:也被稱爲引導(原始)類加載器,負責加載Java的核心類,並非java.lang.ClassLoader的子類,而是由JVM自身實現的。

(2)擴展類加載器,負責加載JRE的擴展目錄中JAR包的類,經過這種方式,就能夠爲java擴展核心類之外的新功能,只要把本身開發的類打包成JAR文件,而後放入JAVA_HOME/jre/lib/ext路徑便可。

(3)應用程序類加載器,負責在JVM啓動時加載來自java命令的-classpath選項、java.class.path系統屬性,或CLASSPATH環境變量所指定的JAR包和類路徑。若是沒有特別指定,用戶自定義的類加載器都以系統類加載器做爲父加載器。

如何建立並使用自定義的類加載器?

繼承ClassLoader並重寫其findClass()方法。

URLClassLoader---系統類加載器和擴展類加載器的父類(繼承關係),既能夠從本地文件系統獲取二進制文件來加載類,也能夠從遠程主機獲取二進制文件來加載類。

雙親委派模型

若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的父加載器都是如此,所以全部的請求最終都應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋本身沒法完成這個加載請求時,子加載器纔會嘗試本身去加載。雙親委派模型對於保證JAVA程序的穩定運做很重要。

12. 分派:靜態分派與動態分派

(暫時跳過)

13.Java內存模型JMM

定義了一些規則,保證多個線程間能夠高效地、正確地協同工做,關鍵的工做圍繞多線程的原子性、可見性和有序性展開。

(1)原子性指一個操做不可被中斷

(2)可見性是指一個線程修改了共享變量的值,其餘線程是否能知道這個修改

(3)有序性是指程序在執行的過程當中會發生指令重排,指令重排能夠保證串行語義的一致性,但不能保證多線程下的語義也一致。

*爲什麼進行指令重排?

儘可能少的中斷流水線,提升CPU性能。

哪些指令不能進行重排?(Happen-before規則)

(1)程序順序原則:一個線程內保證語義的串行性

(2)volatile規則:volatile變量的寫先於讀,保證了volatile變量的可見性

(3)鎖規則:解鎖(unlock)先於加鎖(lock)

......(詳見:《實戰Java高併發程序設計》)

以上原則保證了指令重排不會破壞原有的語義結構。

14.JAVA 四中引用類型

  • 強引用

在 Java 中最多見的就是強引用,把一個對象賦給一個引用變量,這個引用變量就是一個強引 用。當一個對象被強引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即便該對象之後永遠都不會被用到 JVM 也不會回收。所以強引用是形成 Java 內存泄漏的主要緣由之 一。

  • 軟引用

軟引用須要用 SoftReference 類來實現,對於只有軟引用的對象來講,當系統內存足夠時它不會被回收,當系統內存空間不足時它會被回收。軟引用一般用在對內存敏感的程序中。

  • 弱引用

弱引用須要用 WeakReference 類來實現,它比軟引用的生存期更短,對於只有弱引用的對象來講,只要垃圾回收機制一運行,無論JVM 的內存空間是否足夠,總會回收該對象佔用的內存

  • 虛引用

虛引用須要 PhantomReference 類來實現,它不能單獨使用,必須和引用隊列聯合使用。虛引用的主要做用是對象在被收集器收集時收到一個系統通知

15. 逃逸分析

淺談HotSpot逃逸分析

相關文章
相關標籤/搜索