一、JVM整體結(jié)構(gòu)及內(nèi)存模型
(查看指令碼:javap -c/v matn.class > math.txt)-jvap -v可看常量池
本地方法棧:存放c源碼方法,比如:Thread.start().start0(),native修飾
棧線程:存放當(dāng)前線程變量以及方法內(nèi)存,如main方法,compute方法,每個(gè)方法為一個(gè)棧幀
- 局部變量:存放變量,從1開始,0為this該類對象
- 操作數(shù)棧:將變量值取出來進(jìn)行加減乘除操作
- 動態(tài)鏈接:程序加載過程中,將符號引用改為直接引用
-方法出口:記錄當(dāng)前方法執(zhí)行完成后應(yīng)該返回到主方法第幾步繼續(xù)執(zhí)行主方法,相當(dāng)于主方法的程序計(jì)數(shù)器
堆:存放對象
方法區(qū):也叫元空間,加載類信息,存放常量,靜態(tài)變量以及類元信息
程序計(jì)數(shù)器:存儲當(dāng)前線程走到第幾步
字節(jié)碼引擎:1.每執(zhí)行一行代碼程序計(jì)數(shù)器加1,2.執(zhí)行字節(jié)碼

二、JVM內(nèi)存參數(shù)設(shè)置

Spring Boot程序的JVM參數(shù)設(shè)置格式(Tomcat啟動直接加在bin目錄下catalina.sh文件里):
1 java‐Xms2048M‐Xmx2048M‐Xmn1024M‐Xss512K‐XX:MetaspaceSize=256M‐XX:MaxMetaspaceSize=256M‐jarmicroservice‐eurek a‐server.jar
StackOverflowError示例:
publicclassStackOverflowTest{
static int count = 0;
static void redo() { count++;
redo();
public static void main(String[] args) {
12 try{
13 redo();
14 } catch (Throwable t) {
15 t.printStackTrace();
16 System.out.println(count);
17 }
18 }
19 }
20
21 運(yùn)行結(jié)果:
22 java.lang.StackOverflowError
23 at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:12)
24 at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
25 at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
結(jié)論: -Xss設(shè)置越小count值越小,說明一個(gè)線程棧里能分配的棧幀就越少,但是對JVM整體來說能開啟的線程數(shù)會更多
JVM內(nèi)存參數(shù)大小該如何設(shè)置? JVM參數(shù)大小設(shè)置并沒有固定標(biāo)準(zhǔn),需要根據(jù)實(shí)際項(xiàng)目情況分析,給大家舉個(gè)例子
日均百萬級訂單交易系統(tǒng)如何設(shè)置JVM參數(shù)

一天百萬級訂單這個(gè)絕對是現(xiàn)在頂尖電商公司交易量級,對于這種量級的系統(tǒng)我們該如何設(shè)置JVM參數(shù)了?
我們可以試著估算下,其實(shí)日均百萬訂單主要也就是集中在當(dāng)日的幾個(gè)小時(shí)生成的,我們假設(shè)是三小時(shí),也就是每秒大概生成100單左 右。
這種系統(tǒng)我們一般至少要三四臺機(jī)器去支撐,假設(shè)我們部署了四臺機(jī)器,也就是每臺每秒鐘大概處理完成25單左右,往上毛估每秒處理30 單吧。
也就是每秒大概有30個(gè)訂單對象在堆空間的新生代內(nèi)生成,一個(gè)訂單對象的大小跟里面的字段多少及類型有關(guān),比如int類型的訂單id和用 戶id等字段,double類型的訂單金額等,int類型占用4字節(jié),double類型占用8字節(jié),初略估計(jì)下一個(gè)訂單對象大概1KB左右,也就是說 每秒會有30KB的訂單對象分配在新生代內(nèi)。
真實(shí)的訂單交易系統(tǒng)肯定還有大量的其他業(yè)務(wù)對象,比如購物車、優(yōu)惠券、積分、用戶信息、物流信息等等,實(shí)際每秒分配在新生代內(nèi)的 對象大小應(yīng)該要再擴(kuò)大幾十倍,我們假設(shè)30倍,也就是每秒訂單系統(tǒng)會往新生代內(nèi)分配近1M的對象數(shù)據(jù),這些數(shù)據(jù)一般在訂單提交完的 操作做完之后基本都會成為垃圾對象。
我們一般線上服務(wù)器的配置用得較多的就是雙核4G或4核8G,如果我們用雙核4G的機(jī)器,因?yàn)榉?wù)器操作系統(tǒng)包括一些后臺服務(wù)本身可 能就要占用1G多內(nèi)存,也就是說給JVM進(jìn)程最多分配2G多點(diǎn)內(nèi)存,刨開給方法區(qū)和虛擬機(jī)棧分配的內(nèi)存,那么堆內(nèi)存可能也就能分配到 1G多點(diǎn),對應(yīng)的新生代內(nèi)存最后可能就幾百M(fèi),那么意味著沒過幾百秒新生代就會被垃圾對象撐滿而出發(fā)minor gc,這么頻繁的gc對系 統(tǒng)的性能還是有一定影響的。
如果我們選擇4核8G的服務(wù)器,就可以給JVM進(jìn)程分配四五個(gè)G的內(nèi)存空間,那么堆內(nèi)存可以分到三四個(gè)G左右,于是可以給新生代至少分 配2G,這樣算下差不多需要半小時(shí)到一小時(shí)才能把新生代放滿觸發(fā)minor gc,這就大大降低了minor gc的頻率,所以一般我們線上服務(wù) 器用得較多的還是4核8G的服務(wù)器配置。
如果系統(tǒng)業(yè)務(wù)量繼續(xù)增長那么可以水平擴(kuò)容增加更多的機(jī)器,比如五臺甚至十臺機(jī)器,這樣每臺機(jī)器的JVM處理請求可以保證在合適范 圍,不至于壓力過大導(dǎo)致大量的gc。
有的同學(xué)可能有疑問說雙核4G的服務(wù)器好像也夠用啊,無非就是minor gc頻率稍微高一點(diǎn)呀,不是說minor gc對系統(tǒng)的影響不是特別大 嗎,我成本有限,只能用這樣的服務(wù)器啊。
其實(shí)如果系統(tǒng)業(yè)務(wù)量比較平穩(wěn)也能湊合用,如果經(jīng)常業(yè)務(wù)量可能有個(gè)幾倍甚至幾十倍的增長,比如時(shí)不時(shí)的搞個(gè)促銷秒殺活動什么的,那 我們思考下會不會有什么問題。
假設(shè)業(yè)務(wù)量暴增幾十倍,在不增加機(jī)器的前提下,整個(gè)系統(tǒng)每秒要生成幾千個(gè)訂單,之前每秒往新生代里分配的1M對象數(shù)據(jù)可能增長到 幾十M,而且因?yàn)橄到y(tǒng)壓力驟增,一個(gè)訂單的生成不一定能在1秒內(nèi)完成,可能要幾秒甚至幾十秒,那么就有很多對象會在新生代里存活 幾十秒之后才會變?yōu)槔鴮ο?,如果新生代只分配了幾百M(fèi),意味著一二十秒就會觸發(fā)一次minor gc,那么很有可能部分對象就會被挪到 老年代,這些對象到了老年代后因?yàn)閷?yīng)的業(yè)務(wù)操作執(zhí)行完畢,馬上又變?yōu)榱死鴮ο?,隨著系統(tǒng)不斷運(yùn)行,被挪到老年代的對象會越來 越多,最終可能又會導(dǎo)致full gc,full gc對系統(tǒng)的性能影響還是比較大的。 如果我們用的是4核8G的服務(wù)器,新生代分配到2G以上的水平,那么至少也要幾百秒才會放滿新生代觸發(fā)minor gc,那些在新生代即便存 活幾十秒的對象在minor gc觸發(fā)的時(shí)候大部分已經(jīng)變?yōu)槔鴮ο罅?,都可以被及時(shí)回收,基本不會被挪到老年代,這樣可以大大減少老年 代的full gc次數(shù)。

三、逃逸分析 JVM的運(yùn)行模式有三種:
- 解釋模式(Interpreted Mode):只使用解釋器(-Xint 強(qiáng)制JVM使用解釋模式),執(zhí)行一行JVM字節(jié)碼就編譯一行為機(jī)器碼
- 編譯模式(Compiled Mode):只使用編譯器(-Xcomp JVM使用編譯模式),先將所有JVM字節(jié)碼一次編譯為機(jī)器碼,然 后一次性執(zhí)行所有機(jī)器碼
- 混合模式(Mixed Mode):依然使用解釋模式執(zhí)行代碼,但是對于一些 "熱點(diǎn)" 代碼采用編譯模式執(zhí)行,JVM一般采用混合模 式執(zhí)行代碼
解釋模式啟動快,對于只需要執(zhí)行部分代碼,并且大多數(shù)代碼只會執(zhí)行一次的情況比較適合;編譯模式啟動慢,但是后期執(zhí)行速度快,而 且比較占用內(nèi)存,因?yàn)闄C(jī)器碼的數(shù)量至少是JVM字節(jié)碼的十倍以上,這種模式適合代碼可能會被反復(fù)執(zhí)行的場景;混合模式是JVM默認(rèn)采 用的執(zhí)行代碼方式,一開始還是解釋執(zhí)行,但是對于少部分 “熱點(diǎn) ”代碼會采用編譯模式執(zhí)行,這些熱點(diǎn)代碼對應(yīng)的機(jī)器碼會被緩存起 來,下次再執(zhí)行無需再編譯,這就是我們常見的JIT(Just In Time Compiler)即時(shí)編譯技術(shù)。 在即時(shí)編譯過程中JVM可能會對我們的代碼最一些優(yōu)化,比如對象逃逸分析等
對象逃逸分析:就是分析對象動態(tài)作用域,當(dāng)一個(gè)對象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他地方 中。
1 publicUsertest1(){
2 User user = new User();
3 user.setId(1);
4 user.setName("zhuge");
5 //TODO 保存到數(shù)據(jù)庫
6 return user;
7}
8
9 publicvoidtest2(){
10 11 12 13 14
User user = new User(); user.setId(1); user.setName("zhuge"); //TODO 保存到數(shù)據(jù)庫
}
很顯然test1方法中的user對象被返回了,這個(gè)對象的作用域范圍不確定,test2方法中的user對象我們可以確定當(dāng)方法結(jié)束這個(gè)對象就可 以認(rèn)為是無效對象了,對于這樣的對象我們其實(shí)可以將其分配的棧內(nèi)存里,讓其在方法結(jié)束時(shí)跟隨棧內(nèi)存一起被回收掉。 JVM對于這種情況可以通過開啟逃逸分析參數(shù)(-XX:+DoEscapeAnalysis)來優(yōu)化對象內(nèi)存分配位置,JDK7之后默認(rèn)開啟逃逸分析,如果要 關(guān)閉使用參數(shù)(-XX:-DoEscapeAnalysis)