jvm是一個比較高深的技術,本人也是緊跟周陽老師的視頻走的,java
此文章轉 https://www.jianshu.com/p/9e6841a895b4web
- 友情連接:JVM調參、GCRoots與四大引用淺析
注意:咱們平時說的棧是指的Java棧,native method stack 裏面裝的都是native方法。見下文面試
注意:算法
- 方法區並非存放方法的區域,其是存放類的描述信息(模板)的地方
- Class loader只是負責class文件的加載,至關於快遞員,這個「快遞員」並非只有一家,Class loader有多種
- 加載以前是「小class」,加載以後就變成了「大Class」,這是安裝java.lang.Class模板生成了一個實例。「大Class」就裝載在方法區,模板實例化以後就獲得n個相同的對象
- JVM並非經過檢查文件後綴是否是
.class
來判斷是否須要加載的,而是經過文件開頭的特定文件標誌
注意:數組
- Class loader有多種,能夠說三個,也能夠說是四個(第四個爲本身定義的加載器,繼承 ClassLoader),系統自帶的三個分別爲:
- 啓動類加載器(Bootstrap) ,C++所寫
- 擴展類加載器(Extension) ,Java所寫
- 應用程序類加載器(AppClassLoader)。
咱們本身new的時候建立的是應用程序類加載器(AppClassLoader)。安全
import com.gmail.fxding2019.T; public class Test{ //Test:查看類加載器 public static void main(String[] args) { Object object = new Object(); //查看是那個「ClassLoader」(快遞員把Object加載進來的) System.out.println(object.getClass().getClassLoader()); //查看Object的加載器的上一層 // error Exception in thread "main" java.lang.NullPointerException(已是祖先了) //System.out.println(object.getClass().getClassLoader().getParent()); System.out.println(); Test t = new Test(); System.out.println(t.getClass().getClassLoader().getParent().getParent()); System.out.println(t.getClass().getClassLoader().getParent()); System.out.println(t.getClass().getClassLoader()); } } /* *output: * null * * null * sun.misc.Launcher$ExtClassLoader@4554617c * sun.misc.Launcher$AppClassLoader@18b4aac2 * */
注意:併發
- 若是是JDK自帶的類(Object、String、ArrayList等),其使用的加載器是Bootstrap加載器;若是本身寫的類,使用的是AppClassLoader加載器;Extension加載器是負責將把java更新的程序包的類加載進行
- 輸出中,sun.misc.Launcher是JVM相關調用的入口程序
- Java加載器個數爲3+1。前三個是系統自帶的,用戶能夠定製類的加載方式,經過繼承Java. lang. ClassLoader
注意:jvm
- 雙親委派機制:「我爸是李剛,有事找我爹」。
例如:須要用一個A.java這個類,首先去頂部Bootstrap根加載器去找,找獲得你就用,找不到再降低一層,去Extension加載器去找,找獲得就用,找不到再將一層,去AppClassLoader加載器去找,找獲得就用,找不到就會報"CLASS NOT FOUND EXCEPTION"。
//測試加載器的加載順序 package java.lang; public class String { public static void main(String[] args) { System.out.println("hello world!"); } } /* * output: * 錯誤: 在類 java.lang.String 中找不到 main 方法 * */
上面代碼是爲了測試加載器的順序:首先加載的是Bootstrap加載器,因爲JVM中有java.lang.String這個類,因此會首先加載這個類,而不是本身寫的類,而這個類中並沒有main方法,因此會報「在類 java.lang.String 中找不到 main 方法」。ide
這個問題就涉及到,若是有兩個相同的類,那麼java到底會用哪個?若是使用用戶本身定義的java.lang.String,那麼別使用這個類的程序會去所有出錯,因此,爲了保證用戶寫的源代碼不污染java出廠自帶的源代碼,而提供了一種「雙親委派」機制,保證「沙箱安全」。即先找到先使用。佈局
Thread類的start方法以下:
public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } private native void start0();
Thread類中居然有一個只有聲明沒有實現的方法,並使用native
關鍵字。用native表示,也此方法是系統級(底層操做系統或第三方C語言)的,而不是語言級的,java並不能對其進行操做。native方法裝載在native method stack中。
上面圖中是亮色的地方有兩個特色:
- 全部線程共享(灰色是線程私有)
- 亮色地方存在垃圾回收
注意:
- 方法區:絕對不是放方法的地方,他是存儲的每個類的結構信息(好比static)
- 永久代和元空間的解釋:
方法區是一種規範,相似於接口定義的規範:List list = new ArrayList();
把這種比喻用到方法區則有:
- java 7中:
方法區 f = new 永久代();
- java 8中:
方法去 f = new 元空間();
注意:
- 棧管運行,堆管存儲
- 棧是線程私有,不存在垃圾回收
- 棧幀的概念:java中的方法被扔進虛擬機的棧空間以後就成爲「棧幀」,好比main方法,是程序的入口,被壓棧以後就成爲棧幀。
public class Test{ public static void m(){ m(); } public static void main(String[] args) { System.out.println("111"); //Exception in thread "main" java.lang.StackOverflowError m(); System.out.println("222"); } } /* *output: * 111 * Exception in thread "main" java.lang.StackOverflowError * */
注意:
- StackOverflowError是一個「」錯誤,而不是「異常」。
注意:
HotSpot:若是沒有明確指明,JDK的名字就叫HotSpot
元數據:描述數據的數據(即模板,也就是「大Class」)
上面的關係圖的一個實例爲下圖:
注意:
- Java 7以前和圖上如出一轍,Java 8把永久區換成了元空間
- 堆邏輯上由」新生+養老+元空間「三個部分組成,物理上由」新生+養老「兩個部分組成
- 當執行
new Person();
時,實際上是new在新生區的伊甸園區,而後往下走,走到養老區,可是並未到元空間。
注意:
- GC發生在伊甸園區,當對象快佔滿新生代時,就會發生YGC(Young GC,輕量級GC)操做,伊甸園區基本所有清空
- 倖存者0區(S0),別名「from區」。伊甸園區沒有被YGC清空的對象將移至倖存者0區,倖存者1區別名「to 區」
- 每次進行YGC操做,倖存的對象就會從伊甸園區移到倖存者0區,若是倖存者0區滿了,就會繼續往下移,若是經歷數次YGC操做對象尚未消亡,最終會來到養老區
- 若是到最後,養老區也滿了,那麼就對養老區進行FGC(Full GC,重GC),對養老區進行清洗
- 若是進行了屢次FGC以後,仍是沒法騰出養老區的空間,就會報OOM(out of Memory)異常
- from區和to區位置和名分不是固定的,每次GC事後都會交換,GC交換後,誰空誰是to區
注意:
- 整個堆分爲新生區和養老區,新生區佔整個堆的1/3,養老區佔2/3。新生區又分爲3份:伊甸園區:倖存者0區(from區):倖存者1區(to區) = 8:1:1
- 每次從伊甸園區通過GC倖存的對象,年齡(代數)會+1
注意:
- 臨時對象就是說明,其在伊甸園區生,也在伊甸園區死。
- 堆邏輯上由」新生+養老+元空間「三個部分組成,物理上由」新生+養老「兩個部分組成,元空間也叫方法區
- 永久代(方法區)幾乎沒有垃圾回收,裏面存放的都是加載的rt.jar等,讓你隨時可用
注意
- 上面的圖展現的是物理上的堆,分爲兩塊,新生區和養老區。
- 堆的參數主要有兩個:
-Xms
,Xmx
:
-Xms
堆的初始化的大小Xmx
堆的最大化- Young Gen(新生代)有一個參數
-Xmn
,這個參數能夠調新生區和養老區的比例。可是,這個參數通常不調。- 永久代也有兩個參數:
-XX:PermSize
,-XX:MaxPermSize
,能夠分別調永久帶的初始值和最大值。Java 8 後沒有這兩個參數啦,由於Java 8後元空間不在虛擬機內啦,而是在本機物理內存中
//查看本身機器上的默認堆內存和最大堆內存 public class Test{ public static void main(String[] args) { System.out.println(Runtime.getRuntime().availableProcessors()); //返回 Java虛擬機試圖使用的最大內存量。物理內存的1/4(-Xmx) long maxMemory = Runtime.getRuntime().maxMemory() ; //返回 Java虛擬機中的內存總量(初始值)。物理內存的1/64(-Xms) long totalMemory = Runtime.getRuntime().totalMemory() ; System.out.println("MAX_MEMORY =" + maxMemory +"(字節)、" + (maxMemory / (double)1024 / 1024) + "MB"); System.out.println("DEFALUT_MEMORY = " + totalMemory + " (字節)、" + (totalMemory / (double)1024 / 1024) + "MB"); } } /* * 8 MAX_MEMORY =1868038144(字節)、1781.5MB TOTAL_MEMORY = 126877696 (字節)、121.0MB * */
- 注意:JVM參數調優,平時能夠隨便挑初始大小和最大大小,可是實際工做中,初始大小和最大大小應該是一致的,緣由是避免內存忽高忽低產生停頓
- IDEA 的JVM內存配置
點擊Run列表下的Edit Configuration
- 在VM Options中輸入如下參數:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
。
運行程序查看結果
把堆內存調成10M後,再一直new對象,致使Full GC也沒法處理,直至撐爆堆內存,查看堆溢出錯誤(OOM),程序及結果以下:
GC收集日誌信息詳解
第一次進行YGC相關參數:
[PSYoungGen: 2008K->482K(2560K)] 2008K->782K(9728K), 0.0011440 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
最後一次進行FGC相關參數:
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4025K->4005K(7168K)] 4025K->4005K(9216K), [Metaspace: 3289K->3289K(1056768K)], 0.0082055 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
面試題:GC是什麼(分代收集算法)
- 次數上頻繁收集Young區
- 次數上較少收集Old區
- 基本不動元空間
面試題:GC的四大算法(後有詳解)
- 引用計數法
- 複製算法(Copying)
- 標記清除(Mark-Sweep)
- 標記壓縮(Mark-Compact)
面試題:下面程序中,有幾個線程在運行
Answer:有兩個線程,一個是main線程,一個是後臺的gc線程。
知識點:
- JVM在進行GC時,並不是每次都對上面三個內存區域一塊兒回收的,大部分時候回收的都是指新生代。所以GC按照回收的區域又分了兩種類型,一種是普通GC(minor GC or Young GC),一種是全局GC(major GC or Full GC)
- Minor GC和Full GC的區別
普通GC(minor GC):只針對新生代區域的GC,指發生在新生代的垃圾收集動做,由於大多數Java對象存活率都不高,因此Minor GC很是頻繁,通常回收速度也比較快。
全局GC(major GC or Full GC):指發生在老年代的垃圾收集動做,出現了Major GC,常常會伴隨至少一次的Minor GC(但並非絕對的)。Major GC的速度通常要比Minor GC慢上10倍以上 (由於養老區比較大,佔堆的2/3)
代碼示例以下:雖然objectA和objectB都置空,可是他們以前曾發生過相互引用,因此調用system.gc(手動版喚醒GC,後臺也開着自動檔)並不能進行垃圾回收。而且,system.gc執行完以後也不是馬上執行垃圾回收。
注意:在實際工做中,禁用system.gc() !!!
年輕代中使用的是Minor GC(YGC),這種GC算法採用的是複製算法(Copying)。
Minor GC會把Eden中的全部活的對象都移到Survivor區域中,若是Survivor區中放不下,那麼剩下的活的對象就被移到Old generation中,也即一旦收集後,Eden是就變成空的了。
當對象在 Eden ( 包括一個 Survivor 區域,這裏假設是 from 區域 ) 出生後,在通過一次 Minor GC 後,若是對象還存活,而且可以被另一塊 Survivor 區域所容納( 上面已經假設爲 from 區域,這裏應爲 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用複製算法將這些仍然還存活的對象複製到另一塊 Survivor 區域 ( 即 to 區域 ) 中,而後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),而且將這些對象的年齡設置爲1,之後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,經過-XX:MaxTenuringThreshold 來設定參數),這些對象就會成爲老年代。
-XX:MaxTenuringThreshold — 設置對象在新生代中存活的次數
年輕代中的GC,主要是複製算法(Copying)。 HotSpot JVM把年輕代分爲了三部分:1個Eden區和2個Survivor區(分別叫from和to)。默認比例爲8:1:1,通常狀況下,新建立的對象都會被分配到Eden區(一些大對象特殊處理),這些對象通過第一次Minor GC後,若是仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增長1歲,當它的年齡增長到必定程度時,就會被移動到年老代中。由於年輕代中的對象基本都是朝生夕死的(90%以上),因此在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊(from),當這一塊內存用完,就將還活着的對象複製到另一塊上面。複製算法的優勢是不會產生內存碎片,缺點是耗費空間。
在GC開始的時候,對象只會存在於Eden區和名爲「From」的Survivor區,Survivor區「To」是空的。緊接着進行GC,Eden區中全部存活的對象都會被複制到「To」,而在「From」區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到必定值(年齡閾值,能夠經過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到「To」區域。通過此次GC後,Eden區和From區已經被清空。這個時候,「From」和「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」,新的「From」就是上次GC前的「To」。無論怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到「To」區被填滿,「To」區被填滿以後,會將全部對象移動到年老代中。
由於Eden區對象通常存活率較低,通常的,使用兩塊10%的內存做爲空閒和活動區間,而另外80%的內存,則是用來給新建對象分配內存的。一旦發生GC,將10%的from活動區間與另外80%中存活的eden對象轉移到10%的to空閒區間,接下來,將以前90%的內存所有釋放,以此類推。
上面動畫中,Area空閒表明to,Area激活表明from,綠色表明不被回收的,紅色表明被回收的。
複製算法它的缺點也是至關明顯的:
複製算法的缺點就是費空間,其是用在年輕代的,老年代通常是由標記清除或者是標記清除與標記整理的混合實現。
用通俗的話解釋一下標記清除算法,就是當程序運行期間,若可使用的內存被耗盡的時候,GC線程就會被觸發並將程序暫停,隨後將要回收的對象標記一遍,最終統一回收這些對象,完成標記清理工做接下來便讓應用程序恢復運行。
主要進行兩項工做,第一項則是標記,第二項則是清除。
缺點:此算法須要暫停整個應用,會產生內存碎片
標記清除算法小結:
標記壓縮(Mark-Compact)又叫標記清除壓縮(Mark-Sweep-Compact),或者標記清除整理算法。老年代通常是由標記清除或者是標記清除與標記整理的混合實現
面試題:四種算法那個好
Answer:沒有那個算法是能一次性解決全部問題的,由於JVM垃圾回收使用的是分代收集算法,沒有最好的算法,只有根據每一代他的垃圾回收的特性用對應的算法。新生代使用複製算法,老年代使用標記清除和標記整理算法。沒有最好的垃圾回收機制,只有最合適的。
面試題:請說出各個垃圾回收算法的優缺點
- 內存效率:複製算法>標記清除算法>標記整理算法(此處的效率只是簡單的對比時間複雜度,實際狀況不必定如此)。
- 內存整齊度:複製算法=標記整理算法>標記清除算法。
- 內存利用率:標記整理算法=標記清除算法>複製算法。
能夠看出,效率上來講,複製算法是當之無愧的老大,可是卻浪費了太多內存,而爲了儘可能兼顧上面所提到的三個指標,標記/整理算法相對來講更平滑一些,但效率上依然不盡如人意,它比複製算法多了一個標記的階段,又比標記/清除多了一個整理內存的過程
難道就沒有一種最優算法嗎?Java 9 以後出現了G1垃圾回收器,可以解決以上問題,有興趣參考這篇文章。
年輕代特色是區域相對老年代較小,對像存活率低。
這種狀況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對像大小有關,於是很適用於年輕代的回收。而複製算法內存利用率不高的問題,經過hotspot中的兩個survivor的設計獲得緩解。
老年代的特色是區域較大,對像存活率高。
這種狀況,存在大量存活率高的對像,複製算法明顯變得不合適。通常是由標記清除或者是標記清除與標記整理的混合實現。
Mark階段的開銷與存活對像的數量成正比,這點上說來,對於老年代,標記清除或者標記整理有一些不符,但能夠經過多核/線程利用,對併發、並行的形式提標記效率。
Sweep階段的開銷與所管理區域的大小形正相關,但Sweep「就地處決」的特色,回收的過程沒有對像的移動。使其相對其它有對像移動步驟的回收算法,仍然是效率最好的。可是須要解決內存碎片問題。
Compact階段的開銷與存活對像的數據成開比,如上一條所描述,對於大量對像的移動是很大開銷的,作爲老年代的第一選擇並不合適。
基於上面的考慮,老年代通常是由標記清除或者是標記清除與標記整理的混合實現。以hotspot中的CMS回收器爲例,CMS是基於Mark-Sweep實現的,對於對像的回收效率很高,而對於碎片問題,CMS採用基於Mark-Compact算法的Serial Old回收器作爲補償措施:當內存回收不佳(碎片致使的Concurrent Mode Failure時),將採用Serial Old執行Full GC以達到對老年代內存的整理。