jvm調優思路及調優案例
jvm調優思路及調優案例
我們說jvm調優,其實就是不斷測試調整jvm的運行參數,儘可能讓對象都在新生代(Eden)里分配和回收,盡量別讓太多對象頻繁進入老年代,避免頻繁對老年代進行垃圾回收,同時給系統充足的記憶體大小,避免新生代頻繁的進行垃圾回收。從而減少STW(stop the world)的時間。
調優思路
項目運行記憶體分析
我們運行應用程式時,一般會設置一些jvm參數,比如堆記憶體大小,年輕代大小,Eden和Survivor的比例,老年代大小,大對象的閾值,大齡對象進入老年代的閾值等。
而設置這些jvm參數,有2種方式:
- 通過物理記憶體分析設置,比如機器有8G記憶體,假設作業系統分配2-3G,元空間分配256M,堆分配4-5G。
- 通過1設置之後,再通過分析具體的gc日誌來調優。
我們知道jvm有自己的運行時數據區(記憶體模型),其中堆大小,以及堆中的年輕代、老年代的大小比例至關重要,主要就是調整堆中的記憶體比例,運行時數據區(記憶體模型)圖,如下圖:

具體思路
1、分析年輕代對象增長的速率
可以執行命令 jstat -gc pid 1000 10 (每隔1秒執行1次命令,共執行10次),通過觀察EU(eden區的使用)來估算每秒eden大概新增多少對象,如果系統負載不高,可以把頻率1秒換成1分鐘,甚至10分鐘來觀察整體情況。注意,一般系統可能有高峰期和日常期,所以需要在不同的時間分別估算不同情況下對象增長速率。
2、Young GC的觸發頻率和每次耗時
知道年輕代對象增長速率我們就能推根據eden區的大小推算出Young GC大概多久觸發一次,Young GC的平均耗時可以通過 YGCT/YGC 公式算出,根據結果我們大概就能知道系統大概多久會因為Young GC的執行而卡頓多久。
3、每次Young GC後有多少對象存活和進入老年代
這個因為之前已經大概知道Young GC的頻率,假設是每5分鐘一次,那麼可以執行命令 jstat -gc pid 300000 10 ,觀察每次結果eden,survivor和老年代使用的變化情況,在每次gc後eden區使用一般會大幅減少,survivor和老年代都有可能增長,這些增長的對象就是每次Young GC後存活的對象,同時還可以看出每次Young GC後進去老年代大概多少對象,從而可以推算出老年代對象增長速率。
4、Full GC的觸發頻率和每次耗時
知道了老年代對象的增長速率就可以推算出Full GC的觸發頻率了,Full GC的每次耗時可以用公式 FGCT/FGC 計算得出。
總結:盡量讓每次Young GC後的存活對象小於Survivor區域的50%,都留存在年輕代里。盡量別讓對象進入老年代。盡量減少Full GC的頻率,避免頻繁Full GC對JVM性能的影響。
注意:對象進入老年代的幾種方式:
- 大對象
- 對象到達一定年齡閾值
- 動態對象年齡判斷(Young GC後的存活對象小於Survivor區域的50%)
調優案例
案例準備
這裡準備了一個示常式序(demo鏈接),運行以後,我們採用上篇文章介紹到的jstat工具查看各個記憶體gc的情況。
初始JVM參數:
-Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly
根據這些參數,我們知道大體的記憶體模型是這樣的:最快經過6s之後才會發生一次Young GC。

調優分析
示常式序啟動後,我們調用測試類的test()方法:
@RunWith(SpringRunner.class)
@SpringBootTest(classes={Application.class})// 指定啟動類
public class ApplicationTests {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Autowired
private RestTemplate restTemplate;
@Test
public void test() throws Exception {
for (int i = 0; i < 10000; i++) {
String result = restTemplate.getForObject("//localhost:8080/user/process", String.class);
Thread.sleep(1000);
}
}
}
然後觀察整個過程前後,虛擬機的記憶體gc變化:

發現不僅Young GC次數增多了,Full GC的次數也隨著增多,說明對象不僅增長得快,連進入老年代的時間挺快的。
我們回想一下對象進入老年代的幾種方式:
- 大對象(程式碼排除沒有大對象)
- 對象到達一定年齡閾值(通過Young GC觀察沒有達到15次)
- 動態對象年齡判斷(Young GC後的存活對象小於Survivor區域的50%)
所以應該是動態對象年齡判斷機制導致Full GC次數變多了。我們可以嘗試著優化下JVM參數,把年輕代適當調大點。
-Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly
可以通過jinfo查看JVM參數是否生效,優化後的記憶體模型為:

優化後我們再重新跑一下程式,新的gc變化:
優化完發現沒什麼變化,反而是Full GC次數還變多了。

我們思考下full gc 比minor gc還多的原因有哪些?
1、元空間不夠導致的多餘full gc
2、顯示調用System.gc()造成多餘的full gc,這種一般線上盡量通過-XX:+DisableExplicitGC參數禁用,如果加上了這個JVM啟動參數,那麼程式碼中調用System.gc()沒有任何效果
3、老年代空間分配擔保機制
可以簡單排除掉前2個原因,第三個老年代空間擔保機制也可以通過觀察minor gc 與full gc的次數比例進行排除,那接下來就可能真的就是程式產生了很多佔記憶體的對象。我們可以通過jmap、jvisualvm來跟蹤到占記憶體的對象。
jmap -histo 27808

查到了有大量User對象生成,這個可能是問題所在,但不確定,還必須找到對應的程式碼確認,如何找到對應的程式碼有如下幾種方式:
1、程式碼里全文搜索生成User對象的地方(適合只有少數幾處地方的情況)
2、如果生成User對象的地方太多,無法定位具體程式碼,我們可以同時分析下佔用cpu較高的執行緒,一般有大量對象不斷產生,對應的方法程式碼肯定會被頻繁調用,佔用的cpu必然較高,參考上一篇
//www.cnblogs.com/process-h/p/16879018.html
最終定位到的程式碼如下:
@RestController
public class IndexController {
@RequestMapping("/user/process")
public String processUserData() throws InterruptedException {
ArrayList<User> users = queryUsers();
for (User user: users) {
//TODO 業務處理
System.out.println("user:" + user.toString());
}
return "end";
}
/**
* 模擬批量查詢用戶場景
* @return
*/
private ArrayList<User> queryUsers() {
ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 5000; i++) {
users.add(new User(i,"zhuge"));
}
return users;
}
}
public class User {
private int id;
private String name;
// 1024B * 100 = 100KB
byte[] a = new byte[1024*100];
public User(){}
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {return id;}
public void setId(int id) {this.id = id;}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
}
發現User類中定義了一個byte[] a 成員變數,佔了100KB,在queryUsers()中,一次性在記憶體中添加了500M的對象數據,明顯不合適,需要根據上述中的運行時記憶體數據區域閾值進行優化,盡量消除這種朝生夕死的對象導致的full GC.
總結:到這裡,調優案例就結束了,整個過程考慮了jvm的各個調優知識點,相信有心的讀者可以學到一些知識點。


