Java:JVM


 

  • 內存模型
  • 垃圾回收
  • 類加載

 


 

1.GC算法html

根搜索算法、標記-清除算法、複製算法、標記-整理算法java

根搜索算法:設立若干種根對象,當任何一個根對象到某一個對象均不可達時,則認爲這個對象是能夠被回收的。程序員

能夠當作GC roots的對象有如下幾種:算法

一、虛擬機棧中的引用的對象。數據庫

二、方法區中的類靜態屬性引用的對象。apache

三、方法區中的常量引用的對象。編程

四、本地方法棧中JNI的引用的對象。bootstrap

標記-清除算法:當堆中的有效內存空間(available memory)被耗盡的時候,就會中止整個程序(也被成爲stop the world),而後進行兩項工做,第一項則是標記,第二項則是清除。緩存

(1)標記:標記的過程其實就是,遍歷全部的GC Roots,而後將全部GC Roots可達的對象標記爲存活的對象。安全

(2)清除:清除的過程將遍歷堆中全部的對象,將沒有標記的對象所有清除掉。

複製算法:複製算法將內存劃分爲兩個區間,在任意時間點,全部動態分配的對象都只能分配在其中一個區間(稱爲活動區間),而另一個區間(稱爲空閒區間)則是空閒的,當有效內存空間耗盡時,JVM將暫停程序運行,開啓複製算法GC線程。接下來GC線程會將活動區間內的存活對象,所有複製到空閒區間,且嚴格按照內存地址依次排列,與此同時,GC線程將更新存活對象的內存引用地址指向新的內存地址。此時,空閒區間已經與活動區間交換,而垃圾對象如今已經所有留在了原來的活動區間,也就是如今的空閒區間。事實上,在活動區間轉換爲空間區間的同時,垃圾對象已經被一次性所有回收。

標記/整理算法:標記/整理算法與標記/清除算法很是類似,它也是分爲兩個階段:標記和整理。

(1)標記:它的第一個階段與標記/清除算法是如出一轍的,均是遍歷GC Roots,而後將存活的對象標記。

(2)整理:移動全部存活的對象,且按照內存地址次序依次排列,而後將末端內存地址之後的內存所有回收。所以,第二階段才稱爲整理階段。

標記/整理算法不只能夠彌補標記/清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價,標記/整理算法惟一的缺點就是效率也不高,不只要標記全部存活對象,還要整理全部存活對象的引用地址。

參考:JVM學習之GC經常使用算法

 

*2.Java內存模型

 

 

全部線程共享的數據區:堆、方法區;

線程隔離的數據區:虛擬機棧、本地方法棧、程序計數器; 

  1,程序計數器(Program Counter Register):程序計數器是一個比較小的內存區域,用於指示當前線程所執行的字節碼執行到了第幾行,能夠理解爲是當前線程的行號指示器。字節碼解釋器在工做時,會經過改變這個計數器的值來取下一條語句指令。每一個程序計數器只用來記錄一個線程的行號,因此它是線程私有(一個線程就有一個程序計數器)的。

  2,虛擬機棧(JVM Stack):一個線程的每一個方法在執行的同時,都會建立一個棧幀(Statck Frame),棧幀中存儲的有局部變量表、操做數棧、動態連接、方法出口等,當方法被調用時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。每一個線程對應着一個虛擬機棧,所以虛擬機棧也是線程私有的。

  3,本地方法棧(Native Method Statck):本地方法棧在做用,運行機制,異常類型等方面都與虛擬機棧相同,惟一的區別是:虛擬機棧是執行Java方法的,而本地方法棧是用來執行native方法的,在不少虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將本地方法棧與虛擬機棧放在一塊兒使用。本地方法棧也是線程私有的。

  4,堆區(Heap):堆區是理解Java GC機制最重要的區域,沒有之一。在JVM所管理的內存中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要內存區域,堆區由全部線程共享,在虛擬機啓動時建立。堆區的存在是爲了存儲對象實例,原則上講,全部的對象都在堆區上分配內存(不過現代技術裏,也不是這麼絕對的,也有棧上直接分配的)。

  5,方法區(Method Area):方法區是各個線程共享的區域,用於存儲已經被虛擬機加載的類信息(即加載類時須要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態變量、編譯器即時編譯的代碼等。

參考:Java內存管理Java內存與GC

 

 

*3.內存分配及GC機制

這裏所說的內存分配,主要指的是在堆上的分配。Java內存分配和回收的機制歸納的說,就是:分代分配,分代回收。對象將根據存活的時間被分爲:年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法區)。

  • 年輕代(Young Generation):對象被建立時,內存的分配首先發生在年輕代(大對象能夠直接被建立在年老代),大部分的對象在建立後很快就再也不使用,所以很快變得不可達,因而被年輕代的GC機制清理掉(IBM的研究代表,98%的對象都是很快消亡的),這個GC機制被稱爲Minor GC或叫Young GC。注意,Minor GC並不表明年輕代內存不足,它事實上只表示在Eden區上的GC。

  年輕代上的內存分配是這樣的,年輕代能夠分爲3個區域:Eden區(伊甸園,亞當和夏娃偷吃禁果生娃娃的地方,用來表示內存首次分配的區域,再貼切不過)和兩個存活區(Survivor 0 、Survivor 1)。

  1. 絕大多數剛建立的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的內存空間,所以在其上分配內存極快;
  2. 最初一次,當Eden區滿的時候,執行Minor GC,將消亡的對象清理掉,並將剩餘的對象複製到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);
  3.  下次Eden區滿了,再執行一次Minor GC,將消亡的對象清理掉,將存活的對象複製到Survivor1中,而後清空Eden區;
  4.  將Survivor0中消亡的對象清理掉,將其中能夠晉級的對象晉級到Old區,將存活的對象也複製到Survivor1區,而後清空Survivor0區;
  5. 當兩個存活區切換了幾回(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代,但這只是個最大值,並不表明必定是這個值)以後,仍然存活的對象(其實只有一小部分,好比,咱們本身定義的對象),將被複制到老年代。

  從上面的過程能夠看出,Eden區是連續的空間,且Survivor總有一個爲空。通過一次GC和複製,一個Survivor中保存着當前還活着的對象,而Eden區和另外一個Survivor區的內容都再也不須要了,能夠直接清空,到下一次GC時,兩個Survivor的角色再互換。所以,這種方式分配內存和清理內存的效率都極高,這種垃圾回收的方式就是著名的中止-複製(Stop-and-copy)」清理法(將Eden區和一個Survivor中仍然存活的對象拷貝到另外一個Survivor中),這不表明着中止複製清理法很高效,其實,它也只在這種狀況下高效,若是在老年代採用中止複製,則挺悲劇的。

 

  • 年老代(Old Generation):對象若是在年輕代存活了足夠長的時間而沒有被清理掉(即在幾回Young GC後存活了下來),則會被複制到年老代,年老代的空間通常比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時,將執行Major GC,也叫 Full GC。

  老年代存儲的對象比年輕代多得多,並且不乏大對象,對老年代進行內存清理時,若是使用中止-複製算法,則至關低效。通常,老年代用的算法是標記-整理算法,即:標記出仍然存活的對象(存在引用的),將全部存活的對象向一端移動,以保證內存的連續。

 

  • 方法區(永久代):

  永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就能夠被回收。對於無用的類進行回收,必須保證3點:

  1. 類的全部實例都已經被回收
  2. 加載類的ClassLoader已經被回收
  3. 類對象的Class對象沒有被引用(即沒有經過反射引用該類的地方)

 

參考:常見GC問題Java內存與GC

 

4.GC是在何時,對什麼東西,作了什麼事情?

  1.能說明minor gc/full gc的觸發條件、OOM的觸發條件,下降GC的調優的策略。
  分析:列舉一些我指望的回答:eden滿了minor gc,升到老年代的對象大於老年代剩餘空間full gc,或者小於時被HandlePromotionFailure參數強制full gc;gc與非gc時間耗時超過了GCTimeRatio的限制引起OOM,調優諸如經過NewRatio控制新生代老年代比例,經過MaxTenuringThreshold控制進入老年前生存次數等
  2.從root搜索不到,並且通過第一次標記、清理後,仍然沒有復活的對象。
  3.能說出諸如新生代作的是複製清理、from survivor、to survivor是幹啥用的、老年代作的是標記清理、標記清理後碎片要不要整理、複製清理和標記清理有有什麼優劣勢等


5. Java和C++在內存分配和管理上有什麼區別?

Java與C++之間有一堵由動態內存分配和垃圾收集技術所圍成的高牆,牆外面的人想進去,牆裏面的人想出來。
對於從事C和C++程序開發的開發人員來講,在內存管理領域,他們既是擁有最高權利的皇帝,也是從事最基礎工做的勞動人民-----既擁有每個對象的全部權,又擔負着每個對象從生命開始到終結的維護責任。
對於Java程序員來講,虛擬機的自動內存分配機制的幫助下,再也不須要爲每個new操做去寫配對的delete/free代碼,並且不容易出現內存泄露和內存溢出問題,看起來由虛擬機管理內存一切都很美好。不過,也正是由於Java程序員把內存控制的權利交給Java虛擬機,一旦出現內存泄露和溢出方面的問題,若是不瞭解虛擬機是怎樣使用內存的,那排查錯誤將會是一項異常艱難的工做。

而且好的Java程序在編寫的時候確定要考慮GC的問題,怎樣定義static對象,怎樣new對象效率更高等等問題,簡稱面向GC的編程

也能夠說Java的內存分配管理是一種託管的方式,託管於JVM。

C++通過編譯時直接編譯成機器碼,而Java是編譯成字節碼,由JVM解釋執行。

C++是編譯型語言,而Java兼具編譯型和解釋型語言的特色。

 

*6.有哪些方法能夠判斷一個對象已經能夠被回收, JVM怎麼判斷一個對象已經消亡能夠被回收?
①引用計數算法
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器都爲0的對象就是不可能再被使用的。
Java語言沒有選用引用計數法來管理內存,由於引用計數法不能很好的解決循環引用的問題。
②根搜索算法
在主流的商用語言中,都是使用根搜索算法來斷定對象是否存活的。
GC Root Tracing 算法思路就是經過一系列的名爲"GC Roots"的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連,即從GC Roots到這個對象不可達,則證實此對象是不可用的。
 
*7.哪些對象能夠做爲GC Roots?
(1.JVM stack;  2/3.方法區;4.本地方法棧)
虛擬機棧(棧幀中的本地變量表)中的引用的對象
方法區中的類靜態屬性引用的對象
方法區中的常量引用的對象
本地方法棧中JNI(Native方法)的引用對象


垃圾收集器

七種垃圾收集器:

Serial(年輕代)、ParNew(年輕代)、Parallel Scavenge(年輕代)、

Serial Old(年老代)、Parallel Old(年老代)、CMS(Concurrent Mark Sweep年老代)、

G1(Garbage Firest)

 
如上圖所示,垃圾回收算法一共有7個,3個屬於年輕代、三個屬於年老代,G1屬於橫跨年輕代和年老代的算法。
JVM會從年輕代和年老代各選出一個算法進行組合, 連線表示哪些算法能夠組合使用
 
PS:JVM有client和server兩個版本,分別針對桌面應用程序和服務端應用作了相應的優化;client版本加載速度快,server版本加載速度較慢但運行速度快;
  • Serial收器 

 

Serial收集器是最基本、發展歷史最悠久的收集器。這個收集器是一個單線程的收集器,當它工做時必須暫停其餘線程的工做,也就是Stop The World。這顯示是它的缺點, 這也是垃圾收集器一直努力的方向。固然,對於相比其它單線程收集器,Serial收集器簡單而高效。對於桌面應用來講,分配的管理內存不會太多,停頓時間徹底能夠控制在幾十毫秒最多一百毫秒之內。因此,Serial收集器對於運行在Client模式下的虛擬機來講是一個很好的選擇。下圖爲Serial結合Serial Old收集器(後續介紹)的運行過程:

 

Serial收集器優勢:簡單而高效,對於限定單個CPU的環境來講,Serial收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到較高的收集效率。Serial收集器依然是Client模式下的默認的新生代垃圾收集器。

 

  • ParNew收集器

 

ParNew收集器其實就是Serial收集器的多線程版本(多CPU下使用效果較好),下圖爲ParNew結合Serial Old收集器(後續介紹)的運行過程:

ParNew收集器對於Serial來講並無太多的創新之處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器,由於除了Serial收集器外,剩下只有它能與CMS收集器(後續介紹)配合工做了。因此,遺憾的是CMS做爲老年代的收集器,卻沒法與JDK1.4中已經存在的新生代收集器Parallel Scavenge配合工做。

 

並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態;

併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上;

 

  • Parallel Scavenge收集器

 

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器,看上去跟ParNew差很少。可是Parallel Scavenge收集器與其餘收集器不一樣在於:CMS等收集器的關注點在於儘量地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂的吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花費1分鐘,那吞吐量就是99%。

Parallel Scavenge收集器也常常稱爲「吞吐量優先」收集器;

停頓時間越短越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。

Parallel Scavenge收集器參數:

-XX:MaxGCPauseMillis參數控制最大垃圾收集停頓時間

以及直接設置吞吐量大小-XX:GCTimeRatio參數。

Parallel Scavenge收集器還有一個-XX:UseAdaptiveSizePolicy開關參數,打開參數後就不須要手動指定新生代的大小、Eden與Survivor區的比例、晉升老年代對象年齡等細節參數了,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整相關參數。這種調節方式叫自適應的調節策略,也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。

 

  • Serial Old收集器

 

Serial Old是一個老年代收集器,它一樣是一個單線程收集器,使用的是「標記-整理」算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。若是在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用;另外一種用途就是做爲CMS收集器的後備方案,在併發收集發生ConCurrent Mode Failure時使用。

 

  • Parallel Old收集器

 

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。這個收集器是在JDK1.6中才開始提供的,在此以前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。緣由是新生代若是選擇了Parallel Scavenge收集器,老年代除了Serial Old收集器別無選擇(由於它沒法與CMS配合使用)。在都CPU時代,因爲Serial Old收集器在服務端性能上的「拖累」,使用了Parallel Scavenge收集器也未必能在總體應用上得到吞吐量最大化的效果。直到Parallel Old收集器的出現後,「吞吐量優先」收集器纔有了比較名副其實的應用組合。在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器,以下圖所示:

 

 

  • CMS收集器

 

CMS(Concurrent Mark Sweep)收集器是一種以得到最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。從名字上就能夠看出,CMS收集器是基於「標記-清除」算法實現的。但它的實際運做過程對於前面幾種收集器來講更復雜一些,整個過程分爲4個步驟:

  • 初始標記(CMS initial mark)
  • 併發標記(CMS concurrent mark)
  • 從新標記(CMS remark)
  • 併發清除(CMS concurrent sweep)

其中,初始標記、從新標記這兩個步驟仍然須要「Stop The World」。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快;併發標記階段就是進行GC Roots Tracing的過程;而從新標記階段則是爲了修正併發標記期間用用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄。這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記時間短。因爲整個過程當中耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的(注意併發與並行的概念),以下圖所示:

CMS是一款優秀的收集器,可是還遠達不到的完美程度,它有如下3個明顯缺點

  • CMS收集器對CPU資源很是敏感。由於在併發階段,它會佔用了一部分線程(或者說CPU資源)而致使應用程序變慢,總吞吐量會下降。
  • CMS收集器沒法處理浮動垃圾,可能出現「Concurrent Mode Failure」失敗而致使另外一次Full GC的產生。因爲CMS併發清理階段用戶線程還在運行着,伴隨着程序運行天然就還會有新的垃圾不斷產生,這部分垃圾出如今標記過程以後,CMS沒法在當次收集中處理它們,只好留待在下一次GC時再清理掉,這一部分垃圾就稱爲「浮動垃圾」。
  • 還有最後一點,CMS是一款基於「標記-清除」算法實現的收集器,這意味着收集結束時會有大量的空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,每每會出現老年代還有很大空間剩餘,可是沒法找到足夠大的鏈接空間來分配當前對象,不得不提早出發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullColletion開關參數(默認是開啓的),用於在CMS收集器頂不住要進行Full GC時開啓內存碎片合併整理過程,內存整理的過程是沒法併發的,空間的碎片問題沒有了,但停頓的時間不得不變長了。虛擬機設計者還提供了另一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,表示每次進入Full GC時都進行碎片整理)。

  

  • G1收集器

 

G1(Garbage First)收集器是當今收集器技術發展的最前沿成果之一,從JDK 6u14中開始就有Early Acsess版本的G1收集器供開發人員實驗、試用,由此開始G1收集器的 「Experimental」 狀態持續了數年時間,直到JDK7u4,Sun公司才認爲它達到足夠成熟的商用程度,移除了「Experimental」的標識。G1是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是將來能夠替換掉JDK1.5中發佈的CMS收集器。其與其它收集器相比,G1具有以下特色:

  • 並行與併發:充分利用多個CPU、多核環境下的硬件優點,使用多個CPU來縮短stop-the-world停頓的時間,經過併發的方式讓Java程序繼續執行;
  • 分代收集:分代概念在G1中依然得以保留。雖然G1能夠不須要其它收集器配合就能獨立管理整個GC堆,但它可以採用不一樣的方式去處理新建立的對象和已經存活了一段時間、熬過屢次GC的舊對象以獲取更好的收集效果。也就是說G1能夠本身管理新生代和老年代了。
  • 空間整合:因爲G1使用了獨立區域(Region)概念,G1從總體來看是基於「標記-整理」算法實現收集,從局部(兩個Region)上來看是基於「複製」算法實現的,但不管如何,這兩種算法都意味着G1運做期間不會產生內存空間碎片。
  • 可預測的停頓:這是G1相對於CMS的另外一大優點,下降停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用這明確指定一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒。

 

與其它收集器相比,G1變化較大的是它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留了新生代和來年代的概念,但新生代和老年代再也不是物理隔離的了它們都是一部分Region(不須要連續)的集合。同時,爲了不全堆掃描,G1使用了Remembered Set來管理相關的對象引用信息。當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏了。

 

若是不計算維護Remembered Set的操做,G1收集器的運做大體可劃分爲如下幾個步驟:

  • 初始標記(Initial Making)
  • 併發標記(Concurrent Marking)
  • 最終標記(Final Marking)
  • 篩選回收(Live Data Counting and Evacuation)

看上去跟CMS收集器的運做過程有幾分類似。初始階段僅僅只是標記一下GC Roots能直接關聯到的對象,而且修改TAMS(Next Top Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確能夠用的Region中建立新對象,這個階段須要停頓線程,但耗時很短。併發標記階段是從GC Roots開始對堆中對象進行可達性分析,找出存活對象,這一階段耗時較長但能與用戶線程併發運行。而最終標記階段須要吧Remembered Set Logs的數據合併到Remembered Set中,這階段須要停頓線程,但可並行執行。最後篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃,這一過程一樣是須要停頓線程的,但Sun公司透露這個階段其實也能夠作到併發,但考慮到停頓線程將大幅度提升收集效率,因此選擇停頓。下圖爲G1收集器運行示意圖:

 
 
七種垃圾收集器簡單比較:

一、Serial收集器

新生代單線程的收集器,採用複製算法,在進行收集垃圾時,必須stop the world

它是虛擬機運行在Client模式下的默認新生代收集器;能夠和Serial Old、CMS組合使用;

二、ParNew收集器

是Serial收集器的多線程版本,採用複製算法,回收時須要stop-the-world;

許多運行在Server模式下的虛擬機中首選的新生代收集器;能夠和Serial Old、CMS組合使用;

除Serial外,只有它能與CMS收集器配合工做;

三、Parallel Scavenge收集器

新生代收集器,使用複製算法又是並行的多線程收集器,關注系統吞吐量

參數:

-XX:MaxGCPauseMillis:最大垃圾收集停頓時間;

-XX:GCTimeRatio:吞吐量大小;

-XX:UseAdaptiveSizePolicy:自適應的調節策略;打開參數後就不須要手動指定新生代的大小、Eden與Survivor區的比例、晉升老年代對象年齡等細節參數了,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整相關參數;

四、Serial Old收集器

是Serial收集器的老年代版本,一樣是單線程收集器,使用標記整理算法。

五、Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用多線程和標記整理算法。

六、CMS收集器(Concurrent Mark Sweep)

老年代收集器;是一種以得到最短回收停頓時間爲目標的收集器,基於標記清除算法。

過程以下:初始標記,併發標記,從新標記,併發清除;其中初始標記和從新標記過程須要stop-the-world;

優勢是併發收集,低停頓,缺點是對CPU資源很是敏感,沒法處理浮動垃圾,收集結束會產生大量空間碎片。 

參數:

UserCMSCompactAtFullCollection:默認開啓,FullGC時進行內存碎片整理,整理時用戶進程需中止,即發生Stop The World
CMSFullGCsBeforeCompaction:設置執行多少次不壓縮的Full GC後,執行一個帶壓縮的(默認爲0,表示每次進入Full GC時都進行碎片整理)

七、G1收集器

是基於標記整理算法實現的,不會產生空間碎片,

過程:初始標記、併發標記、最終標記、篩選回收

優勢:並行與併發、分代收集、空間整合、可預測的停頓;

能夠精確地控制停頓將堆劃分爲多個大小固定的獨立區域,並跟蹤這些區域的垃圾堆積程度,在後臺維護一個優先列表,每次根據容許的收集時間,優先回收垃圾最多的區域(Garbage First)。

 
 參考: 垃圾收集器G1
 

 

*1.ClassLoader類加載器

  • 啓動類加載器:Bootstrap ClassLoader,它負責加載存放在JDK\jre\lib(JDK表明JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,而且能被虛擬機識別的類庫(如rt.jar,全部的java.*開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器是沒法被Java程序直接引用的。
  • 擴展類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的全部類庫(如javax.*開頭的類),開發者能夠直接使用擴展類加載器。
  • 應用程序類加載器:Application ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

 

*2.雙親委派模型

 

雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。

使用雙親委派模型來組織類加載器之間的關係,有一個很明顯的好處,就是Java類隨着它的類加載器(說白了,就是它所在的目錄)一塊兒具有了一種帶有優先級的層次關係,這對於保證Java程序的穩定運做很重要。例如,類java.lang.Object類存放在JDK\jre\lib下的rt.jar之中,所以不管是哪一個類加載器要加載此類,最終都會委派給啓動類加載器進行加載,這便保證了Object類在程序中的各類類加載器中都是同一個類。 

 

3.類加載簡單過程

Java程序運行的場所是內存,當在命令行下執行:
java HelloWorld
命令的時候,JVM會將HelloWorld.class加載到內存中,並造成一個Class的對象HelloWorld.class。
其中的過程就是類加載過程:
  一、尋找jre目錄,尋找jvm.dll,並初始化JVM;
  二、產生一個Bootstrap Loader(啓動類加載器);
  三、Bootstrap Loader自動加載Extended Loader(標準擴展類加載器),並將其父Loader設爲Bootstrap Loader。
  四、Bootstrap Loader自動加載AppClass Loader(系統類加載器),並將其父Loader設爲Extended Loader。
  五、最後由AppClass Loader加載HelloWorld類。
 
4.類加載器的特色
  一、運行一個程序時,老是由AppClass Loader(系統類加載器)開始加載指定的類。
  二、在加載類時,每一個類加載器會將加載任務上交給其父,若是其父找不到,再由本身去加載。
  三、Bootstrap Loader(啓動類加載器)是最頂級的類加載器了,其父加載器爲null.
 
 
5.類的加載
 類加載有三種方式:
  一、命令行啓動應用時候由JVM初始化加載
  二、經過Class.forName()方法動態加載
  三、經過ClassLoader.loadClass()方法動態加載
三種方式區別比較大,看個例子就明白了:
public class HelloWorld { 
        public static void main(String[] args) throws ClassNotFoundException { 
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                // 使用ClassLoader.loadClass()來加載類,不會執行初始化塊 
                loader.loadClass("Test2"); 
                // 使用Class.forName()來加載類,默認會執行初始化塊 
//                Class.forName("Test2"); 
                // 使用Class.forName()來加載類,並指定ClassLoader,初始化時不執行靜態塊 
//                Class.forName("Test2", false, loader); 
        } 
}
 
public class Test2 { 
        static { 
                System.out.println("靜態初始化塊執行了!"); 
        } 
}
Class.forName 是一個靜態方法,一樣能夠用來加載類。該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader) 和Class.forName(String className)。第一種形式的參數 name 表示的是類的全名;initialize 表示是否初始化類;loader 表示加載時使用的類加載器。第二種形式則至關於設置了參數 initialize 的值爲 true,loader 的值爲當前類的類加載器。Class.forName 的一個很常見的用法是在加載數據庫驅動的時候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() 用來加載 ApacheDerby 數據庫的驅動。
 

6.類加載器結構

Java 中的類加載器大體能夠分紅兩類:

一類是系統提供的:

  • 引導類加載器(bootstrapclass loader):它用來加載 Java 的核心庫,是用原生代碼而不是java來實現的,並不繼承自java.lang.ClassLoader,除此以外基本上全部的類加載器都是java.lang.ClassLoader類的一個實例。
  • 擴展類加載器(extensionsclass loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄(通常爲%JRE_HOME%/lib/ext)。該類加載器在此目錄裏面查找並加載 Java 類。
  • 系統類加載器(systemclass loader或 App class loader):它根據當前Java 應用的類路徑(CLASSPATH)來加載 Java 類。通常來講,Java 應用的類都是由它來完成加載的。能夠經過 ClassLoader.getSystemClassLoader() 來獲取它。

另一類則是由 Java 應用開發人員編寫的:

  • 開發人員能夠經過繼承java.lang.ClassLoader 類的方式實現本身的類加載器,以知足一些特殊的需求
 
7.類加載過程

從1.2版本開始,Java引入了雙親委託模型,從而更好的保證Java平臺的安全。在此模型下,當一個裝載器被請求裝載某個類時,它首先委託本身的parent去裝載,若parent能裝載,則返回這個類所對應的Class對象,若parent不能裝載,則由parent的請求者去裝載。(爲何更加安全?由於在此模型下用戶自定義的類裝載器不可能裝載應該由父親裝載器裝載的可靠類,從而防止不可靠甚至惡意的代碼代替由父親裝載器裝載的可靠代碼。)

具體示例:

假如loader2的parent爲loader1,loader1的parent爲system class loader。假設loader2被要求裝載類MyClass,在parent delegation模型下,loader2首先請求loader1代爲裝載,loader1再請求系統類裝載器去裝載MyClass。若系統裝載器能成功裝載,則將MyClass所對應的Class對象的reference返回給loader1,loader1再將reference返回給loader2,從而成功將類MyClass裝載進虛擬機。若系統類裝載器不能裝載MyClass,loader1會嘗試裝載MyClass,若loader1也不能成功裝載,loader2會嘗試裝載。若全部的parent及loader2自己都不能裝載,則裝載失敗。

 
類加載器在成功加載某個類以後,會把獲得的 java.lang.Class 類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,對於一個類加載器實例來講,相同全名的類只加載一次,即 loadClass 方法不會被重複調用。

 

參考:ClassLoader

 

*8.類加載過程

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。它們開始的順序以下圖所示:

 

 其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段。

加載

  加載時類加載過程的第一個階段,在加載階段,虛擬機須要完成如下三件事情:

1.字節流;2.方法區數據結構;3.訪問入口

    一、經過一個類的全限定名來獲取定義此類的二進制字節流。

    二、將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。

    三、在內存中生成一個表明這個類的java.lang.Class對象,做爲對方法區中這些數據的訪問入口。

驗證

    驗證的目的是爲了確保Class文件中的字節流包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。不一樣的虛擬機對類驗證的實現可能會有所不一樣,但大體都會完成如下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。

  • 文件格式的驗證:驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內。通過該階段的驗證後,字節流纔會進入內存的方法區中進行存儲,後面的三個驗證都是基於方法區的存儲結構進行的。
  • 元數據驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合Java語法規範的元數據信息。
  • 字節碼驗證:該階段驗證的主要工做是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會作出危害虛擬機安全的行爲。
  • 符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化爲直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身之外的信息(常量池中的各類符號引用)進行匹配性的校驗。

準備

    準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有如下幾點須要注意:

    一、這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中。

    二、這裏所設置的初始值一般狀況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。

解析

   解析階段是 虛擬機將常量池中的符號引用轉化爲直接引用的過程。在 Class類文件結構一文中已經比較過了符號引用和直接引用的區別和關聯,這裏再也不贅述。前面說解析階段可能開始於初始化以前,也可能在初始化以後開始,虛擬機會根據須要來判斷,究竟是在類被加載器加載時就對常量池中的符號引用進行解析(初始化以前),仍是等到一個符號引用將要被使用前纔去解析它(初始化以後)。
(在java中,一個java類將會編譯成一個class文件。在編譯時,java類並不知道引用類的實際內存地址,所以只能使用符號引用來代替。好比org.simple.People類引用org.simple.Tool類,在編譯時People類並不知道Tool類的實際內存地址,所以只能使用符號org.simple.Tool(假設)來表示Tool類的地址。而在類裝載器裝載People類時,此時能夠經過虛擬機獲取Tool類 的實際內存地址,所以即可以既將符號org.simple.Tool替換爲Tool類的實際內存地址,及直接引用地址。)
(參考: 符號引用與直接引用) 
 
 
初始化
    初始化是類加載過程的最後一步,到了此階段,才真正開始執行類中定義的Java程序代碼。在準備階段,類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程序員經過程序指定的主觀計劃去 初始化類變量和其餘資源,或者能夠從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。
 
參考: 類加載過程
相關文章
相關標籤/搜索