前言
如果在文中用詞或者理解方面出現問題,歡迎指出,此文旨在提及而不深究,但會盡量效率地把知識點都拋出來
一、JVM的基本介紹
JVM 是 Java Virtual Machine 的縮寫,它是一個虛構出來的計算機,一種規范,通過在實際的計算機上仿真模擬各類計算機功能實作···
好,其實拋開這么專業的句子不說,就知道JVM其實就類似于一臺小電腦運行在windows或者linux這些作業系統環境下即可,它直接和作業系統進行互動,與硬體不直接互動,可作業系統可以幫我們完成和硬體進行互動的作業,

1.1 Java檔案是如何被運行的
比如我們現在寫了一個 HelloWorld.java 好了,那這個 HelloWorld.java 拋開所有東西不談,那是不是就類似于一個文本檔案,只是這個文本檔案它寫的都是英文,而且有一定的縮進而已,
那我們的 JVM 是不認識文本檔案的,所以它需要一個 編譯 ,讓其成為一個它會讀二進制檔案的 HelloWorld.class
① 類加載器
如果 JVM 想要執行這個 .class 檔案,我們需要將其裝進一個 類加載器 中,它就像一個搬運工一樣,會把所有的 .class 檔案全部搬進JVM里面來,

② 方法區
方法區 是用于存放類似于元資料資訊方面的資料的,比如類資訊,常量,靜態變數,編譯后代碼···等
類加載器將 .class 檔案搬過來就是先丟到這一塊上
③ 堆
堆 主要放了一些存盤的資料,比如物件實體,陣列···等,它和方法區都同屬于 執行緒共享區域 ,也就是說它們都是 執行緒不安全 的
④ 堆疊
堆疊 這是我們的代碼運行空間,我們撰寫的每一個方法都會放到 堆疊 里面運行,
我們會聽說過 本地方法堆疊 或者 本地方法介面 這兩個名詞,不過我們基本不會涉及這兩塊的內容,它倆底層是使用C來進行作業的,和Java沒有太大的關系,
⑤ 程式計數器
主要就是完成一個加載作業,類似于一個指標一樣的,指向下一行我們需要執行的代碼,和堆疊一樣,都是 執行緒獨享 的,就是說每一個執行緒都會有自己對應的一塊區域而不會存在并發和多執行緒的問題,

小總結
- Java檔案經過編譯后變成 .class 位元組碼檔案
- 位元組碼檔案通過類加載器被搬運到 JVM 虛擬機中
- 虛擬機主要的5大塊:方法區,堆都為執行緒共享區域,有執行緒安全問題,堆疊和本地方法堆疊和計數器都是獨享區域,不存在執行緒安全問題,而 JVM 的調優主要就是圍繞堆,堆疊兩大塊進行
1.2 簡單的代碼例子
一個簡單的學生類

一個main方法

執行main方法的步驟如下:
- 編譯好 App.java 后得到 App.class 后,執行 App.class,系統會啟動一個 JVM 行程,從 classpath 路徑中找到一個名為 App.class 的二進制檔案,將 App 的類資訊加載到運行時資料區的方法區內,這個程序叫做 App 類的加載
- JVM 找到 App 的主程式入口,執行main方法
- 這個main中的第一條陳述句為 Student student = new Student("tellUrDream") ,就是讓 JVM 創建一個Student物件,但是這個時候方法區中是沒有 Student 類的資訊的,所以 JVM 馬上加載 Student 類,把 Student 類的資訊放到方法區中
- 加載完 Student 類后,JVM 在堆中為一個新的 Student 實體分配記憶體,然后呼叫建構式初始化 Student 實體,這個 Student 實體持有 指向方法區中的 Student 類的型別資訊 的參考
- 執行student.sayName();時,JVM 根據 student 的參考找到 student 物件,然后根據 student 物件持有的參考定位到方法區中 student 類的型別資訊的方法表,獲得 sayName() 的位元組碼地址,
- 執行sayName()
其實也不用管太多,只需要知道物件實體初始化時會去方法區中找類資訊,完成后再到堆疊那里去運行方法,找方法就在方法表中找,
二、類加載器的介紹
之前也提到了它是負責加載.class檔案的,它們在檔案開頭會有特定的檔案標示,將class檔案位元組碼內容加載到記憶體中,并將這些內容轉換成方法區中的運行時資料結構,并且ClassLoader只負責class檔案的加載,而是否能夠運行則由 Execution Engine 來決定
2.1 類加載器的流程
從類被加載到虛擬機記憶體中開始,到釋放記憶體總共有7個步驟:加載,驗證,準備,決議,初始化,使用,卸載,其中驗證,準備,決議三個部分統稱為連接
2.1.1 加載
- 將class檔案加載到記憶體
- 將靜態資料結構轉化成方法區中運行時的資料結構
- 在堆中生成一個代表這個類的 java.lang.Class物件作為資料訪問的入口
2.1.2 連接
- 驗證:確保加載的類符合 JVM 規范和安全,保證被校驗類的方法在運行時不會做出危害虛擬機的事件,其實就是一個安全檢查
- 準備:為static變數在方法區中分配記憶體空間,設定變數的初始值,例如 static int a = 3 (注意:準備階段只設定類中的靜態變數(方法區中),不包括實體變數(堆記憶體中),實體變數是物件初始化時賦值的)
- 決議:虛擬機將常量池內的符號參考替換為直接參考的程序(符號參考比如我現在import java.util.ArrayList這就算符號參考,直接參考就是指標或者物件地址,注意參考物件一定是在記憶體進行)
2.1.3 初始化
初始化其實就是一個賦值的操作,它會執行一個類構造器的
2.1.4 卸載
GC將無用物件從記憶體中卸載
2.2 類加載器的加載順序
加載一個Class類的順序也是有優先級的,類加載器從最底層開始往上的順序是這樣的
- BootStrap ClassLoader:rt.jar
- Extention ClassLoader: 加載擴展的jar包
- App ClassLoader:指定的classpath下面的jar包
- 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.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記憶體會劃分為堆記憶體和非堆記憶體,堆記憶體中也會劃分為年輕代和老年代,而非堆記憶體則為永久代,年輕代又會分為Eden和Survivor區,Survivor也會分為FromPlace和ToPlace,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、-Xmx來調整,也可能是代碼中創建的物件大且多,而且它們一直在被參考從而長時間垃圾收集無法收集它們,

3.3.8 如何判斷一個物件需要被干掉

圖中程式計數器、虛擬機堆疊、本地方法堆疊,3個區域隨著執行緒的生存而生存的,記憶體分配和回收都是確定的,隨著執行緒的結束記憶體自然就被回收了,因此不需要考慮垃圾回收的問題,而Java堆和方法區則不一樣,各執行緒共享,記憶體的分配和回收都是動態的,因此垃圾收集器所關注的都是堆和方法這部分記憶體,
在進行回收前就要判斷哪些物件還存活,哪些已經死去,下面介紹兩個基礎的計算方法
1.參考計數器計算:給物件添加一個參考計數器,每次參考這個物件時計數器加一,參考失效時減一,計數器等于0時就是不會再次使用的,不過這個方法有一種情況就是出現物件的回圈參考時GC沒法回收,
2.可達性分析計算:這是一種類似于二叉樹的實作,將一系列的GC ROOTS作為起始的存活物件集,從這個節點往下搜索,搜索所走過的路徑成為參考鏈,把能被該集合參考到的物件加入到集合中,搜索當一個物件到GC Roots沒有使用任何參考鏈時,則說明該物件是不可用的,主流的商用程式語言,例如Java,C#等都是靠這招去判定物件是否存活的,
(了解一下即可)在Java語言匯總能作為GC Roots的物件分為以下幾種:
- 虛擬機堆疊(堆疊幀中的本地方法表)中參考的物件(區域變數)
- 方法區中靜態變數所參考的物件(靜態變數)
- 方法區中常量參考的物件
- 本地方法堆疊(即native修飾的方法)中JNI參考的物件(JNI是Java虛擬機呼叫對應的C函式的方式,通過JNI函式也可以創建新的Java物件,且JNI對于物件的區域參考或者全域參考都會把它們指向的物件都標記為不可回收)
- 已啟動的且未終止的Java執行緒
這種方法的優點是能夠解決回圈參考的問題,可它的實作需要耗費大量資源和時間,也需要GC(它的分析程序參考關系不能發生變化,所以需要停止所有行程)
3.3.9 如何宣告一個物件的真正死亡
首先必須要提到的是一個名叫 finalize() 的方法
finalize()是Object類的一個方法、一個物件的finalize()方法只會被系統自動呼叫一次,經過finalize()方法逃脫死亡的物件,第二次不會再呼叫,
補充一句:并不提倡在程式中呼叫finalize()來進行自救,建議忘掉Java程式中該方法的存在,因為它執行的時間不確定,甚至是否被執行也不確定(Java程式的不正常退出),而且運行代價高昂,無法保證各個物件的呼叫順序(甚至有不同執行緒中呼叫),在Java9中已經被標記為 deprecated ,且java.lang.ref.Cleaner(也就是強、軟、弱、幻象參考的那一套)中已經逐步替換掉它,會比finalize來的更加的輕量及可靠,

判斷一個物件的死亡至少需要兩次標記
- 如果物件進行可達性分析之后沒發現與GC Roots相連的參考鏈,那它將會第一次標記并且進行一次篩選,判斷的條件是決定這個物件是否有必要執行finalize()方法,如果物件有必要執行finalize()方法,則被放入F-Queue佇列中,
- 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方面的引數,這里就不以一一列舉了
四、關于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,
作者:說出你的愿望吧
鏈接:大白話帶你認識JVM
來源:掘金
著作權歸作者所有,商業轉載請聯系作者獲得授權,非商業轉載請注明出處,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/234598.html
標籤:Java
上一篇:c++資料結構
下一篇:新手求助
