什麼是JVM

1、JVM的基本介紹

JVM 是 Java Virtual Machine 的縮寫,它是一個虛構出來的計算機,一種規範。經過在實際的計算機上仿真模擬各種計算機功能實現···java

好,其實拋開這麼專業的句子不說,就知道JVM其實就相似於一臺小電腦運行在windows或者linux這些操做系統環境下便可。它直接和操做系統進行交互,與硬件不直接交互,可操做系統能夠幫咱們完成和硬件進行交互的工做。
linux

1.1 Java文件是如何被運行的

好比咱們如今寫了一個 HelloWorld.java 好了,那這個 HelloWorld.java 拋開全部東西不談,那是否是就相似於一個文本文件,只是這個文本文件它寫的都是英文,並且有必定的縮進而已。算法

那咱們的 JVM 是不認識文本文件的,因此它須要一個 編譯 ,讓其成爲一個它會讀二進制文件的 HelloWorld.classwindows

① 類加載器

若是 JVM 想要執行這個 .class 文件,咱們須要將其裝進一個 類加載器 中,它就像一個搬運工同樣,會把全部的 .class 文件所有搬進JVM裏面來。
數組

② 方法區

方法區 是用於存放相似於元數據信息方面的數據的,好比類信息,常量,靜態變量,編譯後代碼···等安全

類加載器將 .class 文件搬過來就是先丟到這一塊上數據結構

③ 堆

主要放了一些存儲的數據,好比對象實例,數組···等,它和方法區都同屬於 線程共享區域 。也就是說它們都是 線程不安全多線程

④ 棧

這是咱們的代碼運行空間。咱們編寫的每個方法都會放到 裏面運行。併發

咱們會據說過 本地方法棧 或者 本地方法接口 這兩個名詞,不過咱們基本不會涉及這兩塊的內容,它倆底層是使用C來進行工做的,和Java沒有太大的關係。jvm

⑤ 程序計數器

主要就是完成一個加載工做,相似於一個指針同樣的,指向下一行咱們須要執行的代碼。和棧同樣,都是 線程獨享 的,就是說每個線程都會有本身對應的一塊區域而不會存在併發和多線程的問題。

小總結

  1. Java文件通過編譯後變成 .class 字節碼文件
  2. 字節碼文件經過類加載器被搬運到 JVM 虛擬機中
  3. 虛擬機主要的5大塊:方法區,堆都爲線程共享區域,有線程安全問題,棧和本地方法棧和計數器都是獨享區域,不存在線程安全問題,而 JVM 的調優主要就是圍繞堆,棧兩大塊進行

1.2 簡單的代碼例子

一個簡單的學生類

一個main方法

執行main方法的步驟以下:

  1. 編譯好 App.java 後獲得 App.class 後,執行 App.class,系統會啓動一個 JVM 進程,從 classpath 路徑中找到一個名爲 App.class 的二進制文件,將 App 的類信息加載到運行時數據區的方法區內,這個過程叫作 App 類的加載
  2. JVM 找到 App 的主程序入口,執行main方法
  3. 這個main中的第一條語句爲 Student student = new Student("tellUrDream") ,就是讓 JVM 建立一個Student對象,可是這個時候方法區中是沒有 Student 類的信息的,因此 JVM 立刻加載 Student 類,把 Student 類的信息放到方法區中
  4. 加載完 Student 類後,JVM 在堆中爲一個新的 Student 實例分配內存,而後調用構造函數初始化 Student 實例,這個 Student 實例持有 指向方法區中的 Student 類的類型信息 的引用
  5. 執行student.sayName();時,JVM 根據 student 的引用找到 student 對象,而後根據 student 對象持有的引用定位到方法區中 student 類的類型信息的方法表,得到 sayName() 的字節碼地址。
  6. 執行sayName()

其實也不用管太多,只須要知道對象實例初始化時會去方法區中找類信息,完成後再到棧那裏去運行方法。找方法就在方法表中找。

2、類加載器的介紹

以前也提到了它是負責加載.class文件的,它們在文件開頭會有特定的文件標示,將class文件字節碼內容加載到內存中,並將這些內容轉換成方法區中的運行時數據結構,而且ClassLoader只負責class文件的加載,而是否可以運行則由 Execution Engine 來決定

2.1 類加載器的流程

從類被加載到虛擬機內存中開始,到釋放內存總共有7個步驟:加載,驗證,準備,解析,初始化,使用,卸載。其中驗證,準備,解析三個部分統稱爲鏈接

2.1.1 加載

  1. 將class文件加載到內存
  2. 將靜態數據結構轉化成方法區中運行時的數據結構
  3. 在堆中生成一個表明這個類的 java.lang.Class對象做爲數據訪問的入口

2.1.2 連接

  1. 驗證:確保加載的類符合 JVM 規範和安全,保證被校驗類的方法在運行時不會作出危害虛擬機的事件,其實就是一個安全檢查
  2. 準備:爲static變量在方法區中分配內存空間,設置變量的初始值,例如 static int a = 3 (注意:準備階段只設置類中的靜態變量(方法區中),不包括實例變量(堆內存中),實例變量是對象初始化時賦值的)
  3. 解析:虛擬機將常量池內的符號引用替換爲直接引用的過程(符號引用好比我如今import java.util.ArrayList這就算符號引用,直接引用就是指針或者對象地址,注意引用對象必定是在內存進行)

2.1.3 初始化

初始化其實就是一個賦值的操做,它會執行一個類構造器的< clinit>()方法。由編譯器自動收集類中全部變量的賦值動做,此時準備階段時的那個 static int a = 3 的例子,在這個時候就正式賦值爲3

2.1.4 卸載

GC將無用對象從內存中卸載

2.2 類加載器的加載順序

加載一個Class類的順序也是有優先級的,類加載器從最底層開始往上的順序是這樣的

  1. BootStrap ClassLoader:rt.jar
  2. Extention ClassLoader: 加載擴展的jar包
  3. App ClassLoader:指定的classpath下面的jar包
  4. Custom ClassLoader:自定義的類加載器

2.3 雙親委派機制

當一個類收到了加載請求時,它是不會先本身去嘗試加載的,而是委派給父類去完成,好比我如今要new一個Person,這個Person是咱們自定義的類,若是咱們要加載它,就會先委派App ClassLoader,只有當父類加載器都反饋本身沒法完成這個請求(也就是父類加載器都沒有找到加載所需的Class)時,子類加載器纔會自行嘗試加載

這樣作的好處是,加載位於rt.jar包中的類時無論是哪一個加載器加載,最終都會委託到BootStrap ClassLoader進行加載,這樣保證了使用不一樣的類加載器獲得的都是同一個結果。

其實這個也是一個隔離的做用,避免了咱們的代碼影響了JDK的代碼,好比我如今要來一個

public class String(){
    public static void main(){sout;}
}

這種時候,咱們的代碼確定會報錯,由於在加載的時候實際上是找到了rt.jar中的String.class,而後發現這也沒有main方法

3、運行時數據區

3.1 本地方法棧和程序計數器

好比說咱們如今點開Thread類的源碼,會看到它的start0方法帶有一個native關鍵字修飾,並且不存在方法體,這種用native修飾的方法就是本地方法,這是使用C來實現的,而後通常這些方法都會放到一個叫作本地方法棧的區域。

程序計數器其實就是一個指針,它指向了咱們程序中下一句須要執行的指令,它也是內存區域中惟一一個不會出現OutOfMemoryError的區域,並且佔用內存空間小到基本能夠忽略不計。這個內存僅表明當前線程所執行的字節碼的行號指示器,字節碼解析器經過改變這個計數器的值選取下一條須要執行的字節碼指令。

若是執行的是native方法,那這個指針就不工做了。

3.2 方法區

方法區主要的做用技術存放類的元數據信息,常量和靜態變量···等。當它存儲的信息過大時,會在沒法知足內存分配時報錯。

3.3 虛擬機棧和虛擬機堆

一句話即是:棧管運行,堆管存儲。則虛擬機棧負責運行代碼,而虛擬機堆負責存儲數據。

3.3.1 虛擬機棧的概念

它是Java方法執行的內存模型。裏面會對局部變量,動態鏈表,方法出口,棧的操做(入棧和出棧)進行存儲,且線程獨享。同時若是咱們聽到局部變量表,那也是在說虛擬機棧

public class Person{
    int a = 1;
    
    public void doSomething(){
        int b = 2;
    }
}

3.3.2 虛擬機棧存在的異常

若是線程請求的棧的深度大於虛擬機棧的最大深度,就會報 StackOverflowError (這種錯誤常常出如今遞歸中)。Java虛擬機也能夠動態擴展,但隨着擴展會不斷地申請內存,當沒法申請足夠內存時就會報錯 OutOfMemoryError

3.3.3 虛擬機棧的生命週期

對於棧來講,不存在垃圾回收。只要程序運行結束,棧的空間天然就會釋放了。棧的生命週期和所處的線程是一致的。

這裏補充一句:8種基本類型的變量+對象的引用變量+實例方法都是在棧裏面分配內存。

3.3.4 虛擬機棧的執行

咱們常常說的棧幀數據,說白了在JVM中叫棧幀,放到Java中其實就是方法,它也是存放在棧中的。

棧中的數據都是以棧幀的格式存在,它是一個關於方法和運行期數據的數據集。好比咱們執行一個方法a,就會對應產生一個棧幀A1,而後A1會被壓入棧中。同理方法b會有一個B1,方法c會有一個C1,等到這個線程執行完畢後,棧會先彈出C1,後B1,A1。它是一個先進後出,後進先出原則。

3.3.5 局部變量的複用

局部變量表用於存放方法參數和方法內部所定義的局部變量。它的容量是以Slot爲最小單位,一個slot能夠存放32位之內的數據類型。

虛擬機經過索引定位的方式使用局部變量表,範圍爲[0,局部變量表的slot的數量]。方法中的參數就會按必定順序排列在這個局部變量表中,至於怎麼排的咱們能夠先不關心。而爲了節省棧幀空間,這些slot是能夠複用的,當方法執行位置超過了某個變量,那麼這個變量的slot能夠被其它變量複用。固然若是須要複用,那咱們的垃圾回收天然就不會去動這些內存。

3.3.6 虛擬機堆的概念

JVM內存會劃分爲堆內存和非堆內存,堆內存中也會劃分爲年輕代老年代,而非堆內存則爲永久代。年輕代又會分爲EdenSurvivor區。Survivor也會分爲FromPlaceToPlace,toPlace的survivor區域是空的。Eden,FromPlace和ToPlace的默認佔比爲 8:1:1。固然這個東西其實也能夠經過一個 -XX:+UsePSAdaptiveSurvivorSizePolicy 參數來根據生成對象的速率動態調整

堆內存中存放的是對象,垃圾收集就是收集這些對象而後交給GC算法進行回收。非堆內存其實咱們已經說過了,就是方法區。在1.8中已經移除永久代,替代品是一個元空間(MetaSpace),最大區別是metaSpace是不存在於JVM中的,它使用的是本地內存。並有兩個參數

MetaspaceSize:初始化元空間大小,控制發生GC
MaxMetaspaceSize:限制元空間大小上限,防止佔用過多物理內存。

移除的緣由能夠大體瞭解一下:融合HotSpot JVM和JRockit VM而作出的改變,由於JRockit是沒有永久代的,不過這也間接性地解決了永久代的OOM問題。

3.3.7 Eden年輕代的介紹

當咱們new一個對象後,會先放到Eden劃分出來的一塊做爲存儲空間的內存,可是咱們知道對堆內存是線程共享的,因此有可能會出現兩個對象共用一個內存的狀況。這裏JVM的處理是每一個線程都會預先申請好一塊連續的內存空間並規定了對象存放的位置,而若是空間不足會再申請多塊內存空間。這個操做咱們會稱做TLAB,有興趣能夠了解一下。

當Eden空間滿了以後,會觸發一個叫作Minor GC(就是一個發生在年輕代的GC)的操做,存活下來的對象移動到Survivor0區。Survivor0區滿後觸發 Minor GC,就會將存活對象移動到Survivor1區,此時還會把from和to兩個指針交換,這樣保證了一段時間內總有一個survivor區爲空且to所指向的survivor區爲空。通過屢次的 Minor GC後仍然存活的對象(這裏的存活判斷是15次,對應到虛擬機參數爲 -XX:MaxTenuringThreshold 。爲何是15,由於HotSpot會在對象投中的標記字段裏記錄年齡,分配到的空間僅有4位,因此最多隻能記錄到15)會移動到老年代。老年代是存儲長期存活的對象的,佔滿時就會觸發咱們最常據說的Full GC,期間會中止全部線程等待GC的完成。因此對於響應要求高的應用應該儘可能去減小發生Full GC從而避免響應超時的問題。

並且當老年區執行了full gc以後仍然沒法進行對象保存的操做,就會產生OOM,這時候就是虛擬機中的堆內存不足,緣由可能會是堆內存設置的大小太小,這個能夠經過參數-Xms、-Xms來調整。也多是代碼中建立的對象大且多,並且它們一直在被引用從而長時間垃圾收集沒法收集它們。

補充說明:關於-XX:TargetSurvivorRatio參數的問題。其實也不必定是要知足-XX:MaxTenuringThreshold才移動到老年代。能夠舉個例子:如對象年齡5的佔30%,年齡6的佔36%,年齡7的佔34%,加入某個年齡段(如例子中的年齡6)後,總佔用超過Survivor空間*TargetSurvivorRatio的時候,從該年齡段開始及大於的年齡對象就要進入老年代(即例子中的年齡6對象,就是年齡6和年齡7晉升到老年代),這時候無需等到MaxTenuringThreshold中要求的15

3.3.8 如何判斷一個對象須要被幹掉

圖中程序計數器、虛擬機棧、本地方法棧,3個區域隨着線程的生存而生存的。內存分配和回收都是肯定的。隨着線程的結束內存天然就被回收了,所以不須要考慮垃圾回收的問題。而Java堆和方法區則不同,各線程共享,內存的分配和回收都是動態的。所以垃圾收集器所關注的都是堆和方法這部份內存。

在進行回收前就要判斷哪些對象還存活,哪些已經死去。下面介紹兩個基礎的計算方法

1.引用計數器計算:給對象添加一個引用計數器,每次引用這個對象時計數器加一,引用失效時減一,計數器等於0時就是不會再次使用的。不過這個方法有一種狀況就是出現對象的循環引用時GC無法回收。

2.可達性分析計算:這是一種相似於二叉樹的實現,將一系列的GC ROOTS做爲起始的存活對象集,從這個節點往下搜索,搜索所走過的路徑成爲引用鏈,把能被該集合引用到的對象加入到集合中。搜索當一個對象到GC Roots沒有使用任何引用鏈時,則說明該對象是不可用的。主流的商用程序語言,例如Java,C#等都是靠這招去斷定對象是否存活的。

(瞭解一下便可)在Java語言彙總能做爲GC Roots的對象分爲如下幾種:

  1. 虛擬機棧(棧幀中的本地方法表)中引用的對象(局部變量)
  2. 方法區中靜態變量所引用的對象(靜態變量)
  3. 方法區中常量引用的對象
  4. 本地方法棧(即native修飾的方法)中JNI引用的對象(JNI是Java虛擬機調用對應的C函數的方式,經過JNI函數也能夠建立新的Java對象。且JNI對於對象的局部引用或者全局引用都會把它們指向的對象都標記爲不可回收)
  5. 已啓動的且未終止的Java線程

這種方法的優勢是可以解決循環引用的問題,可它的實現須要耗費大量資源和時間,也須要GC(它的分析過程引用關係不能發生變化,因此須要中止全部進程)

3.3.9 如何宣告一個對象的真正死亡

首先必需要提到的是一個名叫 finalize() 的方法

finalize()是Object類的一個方法、一個對象的finalize()方法只會被系統自動調用一次,通過finalize()方法逃脫死亡的對象,第二次不會再調用。

補充一句:並不提倡在程序中調用finalize()來進行自救。建議忘掉Java程序中該方法的存在。由於它執行的時間不肯定,甚至是否被執行也不肯定(Java程序的不正常退出),並且運行代價高昂,沒法保證各個對象的調用順序(甚至有不一樣線程中調用)。在Java9中已經被標記爲 deprecated ,且java.lang.ref.Cleaner(也就是強、軟、弱、幻象引用的那一套)中已經逐步替換掉它,會比finalize來的更加的輕量及可靠。
  

判斷一個對象的死亡至少須要兩次標記

  1. 若是對象進行可達性分析以後沒發現與GC Roots相連的引用鏈,那它將會第一次標記而且進行一次篩選。判斷的條件是決定這個對象是否有必要執行finalize()方法。若是對象有必要執行finalize()方法,則被放入F-Queue隊列中。
  2. GC對F-Queue隊列中的對象進行二次標記。若是對象在finalize()方法中從新與引用鏈上的任何一個對象創建了關聯,那麼二次標記時則會將它移出「即將回收」集合。若是此時對象還沒成功逃脫,那麼只能被回收了。

若是肯定對象已經死亡,咱們又該如何回收這些垃圾呢

3.4 垃圾回收算法

不會很是詳細的展開,經常使用的有標記清除,複製,標記整理和分代收集算法

3.4.1 標記清除算法

標記清除算法就是分爲「標記」和「清除」兩個階段。標記出全部須要回收的對象,標記結束後統一回收。這個套路很簡單,也存在不足,後續的算法都是根據這個基礎來加以改進的。

其實它就是把已死亡的對象標記爲空閒內存,而後記錄在一個空閒列表中,當咱們須要new一個對象時,內存管理模塊會從空閒列表中尋找空閒的內存來分給新的對象。

不足的方面就是標記和清除的效率比較低下。且這種作法會讓內存中的碎片很是多。這個致使了若是咱們須要使用到較大的內存塊時,沒法分配到足夠的連續內存。好比下圖

此時可以使用的內存塊都是零零散散的,致使了剛剛提到的大內存對象問題

3.4.2 複製算法

爲了解決效率問題,複製算法就出現了。它將可用內存按容量劃分紅兩等分,每次只使用其中的一塊。和survivor同樣也是用from和to兩個指針這樣的玩法。fromPlace存滿了,就把存活的對象copy到另外一塊toPlace上,而後交換指針的內容。這樣就解決了碎片的問題。

這個算法的代價就是把內存縮水了,這樣堆內存的使用效率就會變得十分低下了

不過它們分配的時候也不是按照1:1這樣進行分配的,就相似於Eden和Survivor也不是等價分配是一個道理。

3.4.3 標記整理算法

複製算法在對象存活率高的時候會有必定的效率問題,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存

3.4.4 分代收集算法

這種算法並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或者「標記-整理」算法來進行回收。

說白了就是八仙過海各顯神通,具體問題具體分析了而已。

3.5 (瞭解)各類各樣的垃圾回收器

HotSpot VM中的垃圾回收器,以及適用場景

到jdk8爲止,默認的垃圾收集器是Parallel Scavenge 和 Parallel Old

從jdk9開始,G1收集器成爲默認的垃圾收集器
目前來看,G1回收器停頓時間最短並且沒有明顯缺點,很是適合Web應用。在jdk8中測試Web應用,堆內存6G,新生代4.5G的狀況下,Parallel Scavenge 回收新生代停頓長達1.5秒。G1回收器回收一樣大小的新生代只停頓0.2秒。

3.6 (瞭解)JVM的經常使用參數

JVM的參數很是之多,這裏只列舉比較重要的幾個,經過各類各樣的搜索引擎也能夠得知這些信息。

參數名稱 含義 默認值 說明
-Xms 初始堆大小 物理內存的1/64(<1GB) 默認(MinHeapFreeRatio參數能夠調整)空餘堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制.
-Xmx 最大堆大小 物理內存的1/4(<1GB) 默認(MaxHeapFreeRatio參數能夠調整)空餘堆內存大於70%時,JVM會減小堆直到 -Xms的最小限制
-Xmn 年輕代大小(1.4or lator) 注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不一樣的。整個堆大小=年輕代大小 + 老年代大小 + 持久代(永久代)大小.增大年輕代後,將會減少年老代大小.此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8
-XX:NewSize 設置年輕代大小(for 1.3/1.4)
-XX:MaxNewSize 年輕代最大值(for 1.3/1.4)
-XX:PermSize 設置持久代(perm gen)初始值 物理內存的1/64
-XX:MaxPermSize 設置持久代最大值 物理內存的1/4
-Xss 每一個線程的堆棧大小 JDK5.0之後每一個線程堆棧大小爲1M,之前每一個線程堆棧大小爲256K.更具應用的線程所需內存大小進行 調整.在相同物理內存下,減少這個值能生成更多的線程.可是操做系統對一個進程內的線程數仍是有限制的,不能無限生成,經驗值在3000~5000左右通常小的應用, 若是棧不是很深, 應該是128k夠用的 大的應用建議使用256k。這個選項對性能影響比較大,須要嚴格的測試。(校長)和threadstacksize選項解釋很相似,官方文檔彷佛沒有解釋,在論壇中有這樣一句話:-Xss is translated in a VM flag named ThreadStackSize」通常設置這個值就能夠了
-XX:NewRatio 年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代) -XX:NewRatio=4表示年輕代與年老代所佔比值爲1:4,年輕代佔整個堆棧的1/5Xms=Xmx而且設置了Xmn的狀況下,該參數不須要進行設置。
-XX:SurvivorRatio Eden區與Survivor區的大小比值 設置爲8,則兩個Survivor區與一個Eden區的比值爲2:8,一個Survivor區佔整個年輕代的1/10
-XX:+DisableExplicitGC 關閉System.gc() 這個參數須要嚴格的測試
-XX:PretenureSizeThreshold 對象超過多大是直接在舊生代分配 0 單位字節 新生代採用Parallel ScavengeGC時無效另外一種直接在舊生代分配的狀況是大的數組對象,且數組中無外部引用對象.
-XX:ParallelGCThreads 並行收集器的線程數 此值最好配置與處理器數目相等 一樣適用於CMS
-XX:MaxGCPauseMillis 每次年輕代垃圾回收的最長時間(最大暫停時間) 若是沒法知足此時間,JVM會自動調全年輕代大小,以知足此值.

其實還有一些打印及CMS方面的參數,這裏就不以一一列舉了

4、關於JVM調優的一些方面

根據剛剛涉及的jvm的知識點,咱們能夠嘗試對JVM進行調優,主要就是堆內存那塊

全部線程共享數據區大小=新生代大小 + 年老代大小 + 持久代大小。持久代通常固定大小爲64m。因此java堆中增大年輕代後,將會減少年老代大小(由於老年代的清理是使用fullgc,因此老年代太小的話反而是會增多fullgc的)。此值對系統性能影響較大,Sun官方推薦配置爲java堆的3/8。

4.1 調整最大堆內存和最小堆內存

-Xmx –Xms:指定java堆最大值(默認值是物理內存的1/4(<1GB))和初始java堆最小值(默認值是物理內存的1/64(<1GB))

默認(MinHeapFreeRatio參數能夠調整)空餘堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制.,默認(MaxHeapFreeRatio參數能夠調整)空餘堆內存大於70%時,JVM會減小堆直到 -Xms的最小限制。簡單點來講,你不停地往堆內存裏面丟數據,等它剩餘大小小於40%了,JVM就會動態申請內存空間不過會小於-Xmx,若是剩餘大小大於70%,又會動態縮小不過不會小於–Xms。就這麼簡單

開發過程當中,一般會將 -Xms 與 -Xmx兩個參數的配置相同的值,其目的是爲了可以在java垃圾回收機制清理完堆區後不須要從新分隔計算堆區的大小而浪費資源。

咱們執行下面的代碼

System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");    //系統的最大空間
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");  //系統的空閒空間
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");  //當前可用的總空間

注意:此處設置的是Java堆大小,也就是新生代大小 + 老年代大小

設置一個VM options的參數

-Xmx20m -Xms5m -XX:+PrintGCDetails

再次啓動main方法


這裏GC彈出了一個Allocation Failure分配失敗,這個事情發生在PSYoungGen,也就是年輕代中

這時候申請到的內存爲18M,空閒內存爲4.214195251464844M

咱們此時建立一個字節數組看看,執行下面的代碼

byte[] b = new byte[1 * 1024 * 1024];
System.out.println("分配了1M空間給數組");
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");  //系統的最大空間
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");  //系統的空閒空間
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");

此時free memory就又縮水了,不過total memory是沒有變化的。Java會盡量將total mem的值維持在最小堆內存大小

byte[] b = new byte[10 * 1024 * 1024];
System.out.println("分配了10M空間給數組");
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");  //系統的最大空間
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");  //系統的空閒空間
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");  //當前可用的總空間

這時候咱們建立了一個10M的字節數據,這時候最小堆內存是頂不住的。咱們會發現如今的total memory已經變成了15M,這就是已經申請了一次內存的結果。

此時咱們再跑一下這個代碼

System.gc();
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");    //系統的最大空間
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");  //系統的空閒空間
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");  //當前可用的總空間

此時咱們手動執行了一次fullgc,此時total memory的內存空間又變回5.5M了,此時又是把申請的內存釋放掉的結果。

4.2 調整新生代和老年代的比值

-XX:NewRatio --- 新生代(eden+2*Survivor)和老年代(不包含永久區)的比值

例如:-XX:NewRatio=4,表示新生代:老年代=1:4,即新生代佔整個堆的1/5。在Xms=Xmx而且設置了Xmn的狀況下,該參數不須要進行設置。

4.3 調整Survivor區和Eden區的比值

-XX:SurvivorRatio(倖存代)--- 設置兩個Survivor區和eden的比值

例如:8,表示兩個Survivor:eden=2:8,即一個Survivor佔年輕代的1/10

4.4 設置年輕代和老年代的大小

-XX:NewSize --- 設置年輕代大小

-XX:MaxNewSize --- 設置年輕代最大值

能夠經過設置不一樣參數來測試不一樣的狀況,反正最優解固然就是官方的Eden和Survivor的佔比爲8:1:1,而後在剛剛介紹這些參數的時候都已經附帶了一些說明,感興趣的也能夠看看。反正最大堆內存和最小堆內存若是數值不一樣會致使屢次的gc,須要注意。

4.5 小總結

根據實際事情調整新生代和倖存代的大小,官方推薦新生代佔java堆的3/8,倖存代佔新生代的1/10

在OOM時,記得Dump出堆,確保能夠排查現場問題,經過下面命令你能夠輸出一個.dump文件,這個文件可使用VisualVM或者Java自帶的Java VisualVM工具。

-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=你要輸出的日誌路徑

通常咱們也能夠經過編寫腳本的方式來讓OOM出現時給咱們報個信,能夠經過發送郵件或者重啓程序等來解決。

4.6 永久區的設置

-XX:PermSize -XX:MaxPermSize

初始空間(默認爲物理內存的1/64)和最大空間(默認爲物理內存的1/4)。也就是說,jvm啓動時,永久區一開始就佔用了PermSize大小的空間,若是空間還不夠,能夠繼續擴展,可是不能超過MaxPermSize,不然會OOM。

tips:若是堆空間沒有用完也拋出了OOM,有多是永久區致使的。堆空間實際佔用很是少,可是永久區溢出 同樣拋出OOM。

4.7 JVM的棧參數調優

4.7.1 調整每一個線程棧空間的大小

能夠經過-Xss:調整每一個線程棧空間的大小

JDK5.0之後每一個線程堆棧大小爲1M,之前每一個線程堆棧大小爲256K。在相同物理內存下,減少這個值能生成更多的線程。可是操做系統對一個進程內的線程數仍是有限制的,不能無限生成,經驗值在3000~5000左右

4.7.2 設置線程棧的大小

-XXThreadStackSize:
    設置線程棧的大小(0 means use default stack size)

這些參數都是能夠經過本身編寫程序去簡單測試的,這裏礙於篇幅問題就再也不提供demo了

4.8 (能夠直接跳過了)JVM其餘參數介紹

形形色色的參數不少,就不會說把全部都扯個遍了,由於你們其實也不會說必定要去深究到底。

4.8.1 設置內存頁的大小

-XXThreadStackSize:
    設置內存頁的大小,不可設置過大,會影響Perm的大小

4.8.2 設置原始類型的快速優化

-XX:+UseFastAccessorMethods:
    設置原始類型的快速優化

4.8.3 設置關閉手動GC

-XX:+DisableExplicitGC:
    設置關閉System.gc()(這個參數須要嚴格的測試)

4.8.4 設置垃圾最大年齡

-XX:MaxTenuringThreshold
    設置垃圾最大年齡。若是設置爲0的話,則年輕代對象不通過Survivor區,直接進入年老代.
    對於年老代比較多的應用,能夠提升效率。若是將此值設置爲一個較大值,
    則年輕代對象會在Survivor區進行屢次複製,這樣能夠增長對象再年輕代的存活時間,
    增長在年輕代即被回收的機率。該參數只有在串行GC時纔有效.

4.8.5 加快編譯速度

-XX:+AggressiveOpts

加快編譯速度

4.8.6 改善鎖機制性能

-XX:+UseBiasedLocking

4.8.7 禁用垃圾回收

-Xnoclassgc

4.8.8 設置堆空間存活時間

-XX:SoftRefLRUPolicyMSPerMB
    設置每兆堆空閒空間中SoftReference的存活時間,默認值是1s。

4.8.9 設置對象直接分配在老年代

-XX:PretenureSizeThreshold
    設置對象超過多大時直接在老年代分配,默認值是0。

4.8.10 設置TLAB佔eden區的比例

-XX:TLABWasteTargetPercent
    設置TLAB佔eden區的百分比,默認值是1% 。

4.8.11設置是否優先YGC

-XX:+CollectGen0First
    設置FullGC時是否先YGC,默認值是false。
相關文章
相關標籤/搜索