前言
JVM性能調優是一個很大的話題,很多中小企業的業務規模受限,沒有迫切的性能調優需求,但是如果不知道JVM相關的理論知識,寫出來的代碼或者配置的JVM參數不合理時,就會出現很嚴重的性能問題,到時候開發就會像熱鍋上的螞蟻,等待各方的炙烤。筆者一直在學習JVM相關的理論書籍,看過周志明老師的 深入理解Java虛擬機,也學習過 葛鳴老師的 實戰Java虛擬機 ,但是在實際工作中,只有過寥寥幾次的調優經驗,幾乎無處施展學習到的理論知識,致使知識大部分都存在在筆記和書本中,這次總結面試題,一是希望能夠應對性能調優崗位相關的面試;二是希望總結一下具體的實戰步驟,並努力吸收書中的實踐案例,讓自己的經驗更豐富一些。
JVM性能調優
內存溢出錯誤
學習目的:
- 通過異常信息及時定位到發生內存溢出的運行時數據區域
- 了解什么樣的代碼會導致內存溢出,防止寫出這樣的代碼
- 出現異常后該如何處理,也就是學習事中的處理手段
內存溢出和內存泄露的區別
- 內存泄露:不該留存在進程中的內存數據,雖然很小,但是在經過多次長期的積累后,會導致內存溢出
- 內存溢出:程序申請內存時,內存不足的現象
堆溢出錯誤和預判堆溢出的錯誤
如何復現出堆溢出錯誤?
- JVM參數部分:最大堆和最小堆設置相同並且設置的比較小,比如只有10M,這樣就不會自動擴展堆
- 代碼部分:在一個方法中不斷地往集合中加入元素
代碼實踐
package org.example;
import java.util.ArrayList;
import java.util.List;
/**
* -Xmx10M -Xms10M -XX:+HeapDumpOnOutOfMemoryError
*/
public class App {
static class OOMObject {
int a = 1;
long b = 2;
float c = 2.1f;
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
正確的出現了我們想要的結果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid24476.hprof ...
Heap dump file created [13268403 bytes in 0.077 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at org.example.App.main(App.java:22)
Process finished with exit code 1
如果把參數調大,調整20M,那么會報另外的error
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to java_pid8796.hprof ...
Heap dump file created [27391983 bytes in 0.141 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at org.example.App.main(App.java:19)
Process finished with exit code 1
這個錯誤的原因是,JVMGC時間占據了整個運行時間的98%,但是回收只得到了2%可用的內存,至少出現5次,就會報這個異常。
這個異常是Jdk1.6定義的策略, 通過統計GC時間來預測是否要OOM了,提前拋出異常,防止OOM發生。
案例心得:
- 堆內存溢出的識別:java.lang.OutOfMemoryError: Java heap space 或者 java.lang.OutOfMemoryError: GC overhead limit exceeded
- 死循環中不斷創建對象這種代碼應該規避
- 提前設置好自動轉儲的參數,出現異常能夠恢復現場查看問題
- 事后排查思路:先用JvisualVM這樣的軟件查看具體對象,核查是內存溢出還是內存泄漏,如果確定沒有泄露,需要排查堆的參數設置是否合理,從代碼上分析對象存活時長比較長是否必要,是否可以優化等等。
虛擬機棧和本地方法棧溢出錯誤
一般我們會遇到兩種棧相關的錯誤:
- 單個線程中,不斷的調用方法入棧,當棧深度超過虛擬機所允許的最大深度時,拋出StackOverflowError
- 不斷地創建線程,創建線程就需要創建棧,當無法申請到足夠的內存,就會報 unable to create new native thread錯誤
如何復現?
- JVM參數:-Xss128k,每個線程的棧內存大小
- 代碼部分:沒有出口的遞歸調用
代碼實踐
/**
* -Xss128k
*/
public class App {
static int length = 0;
private static void reverse() {
length++;
reverse();
}
public static void main(String[] args) {
try {
reverse();
} catch (Throwable e) {
System.out.println("length:" + length);
throw e;
}
}
}
結果驗證:
length:1096
Exception in thread "main" java.lang.StackOverflowError
at org.example.App.reverse(App.java:10)
at org.example.App.reverse(App.java:11)
at org.example.App.reverse(App.java:11)
at org.example.App.reverse(App.java:11)
太多了,這里只截取部分
關於unable to create new native thread這個異常,這里就不嘗試了,因為可能會導致操作系統假死等問題。
案例心得:
- 棧錯誤的識別:StackOverflowError 或者 java.lang.OutOfMemoryError: unable to create new native thread
- 沒有出口的遞歸調用要避免;默認的JVM棧大小的參數針對一般的方法調用深度是足夠的
- 如果必須要創建大量的常駐線程,並且是32位的虛擬機,要測試協調好 棧內存和其他內存的大小,防止出現溢出錯誤
- 事后排查思路:先確定是哪種錯誤,然后檢查遞歸調用或者檢查線程數
方法區(元數據區)和運行時常量池溢出
方法區和運行時常量池異常
在JDK1.6以及以前的版本中,運行時常量池是放在方法區中的,我們可以通過限制方法區的大小然后增大常量池來模擬溢出。
如何模擬:
- JDK使用1.6版本,這里注意,要統一idea所有的版本,否則出錯
- 具體細節可以參考這里:idea 啟動時報 error:java 無效的源發行版11
- JVM參數:--XX:PermSize=10M -XX:MaxPermSize=10M
- 應用代碼:使用String.intern方法不斷創建新的常量對象到常量池中,並一直用集合保持強引用
代碼實踐:
package org.example;
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) {
int i = 0;
List list = new ArrayList();
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
結果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at org.example.App.main(App.java from InputFileObject:15)
Process finished with exit code 1
在JDK1.7以后,常量池就被移動到了堆中,所以如果限制了堆的大小,那么最終會報堆溢出異常或者預判堆異常的錯誤的。
同樣的代碼使用JDK1.8版本測試,並指定了堆的最大和初始大小后,果然出現了我預計的異常。
參數:-XX:PermSize=10M -XX:MaxPermSize=10M -Xmx10M -Xms10M
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at org.example.App.main(App.java:13)
如果加上不使用 預判斷限制參數 -XX:-UseGCOverheadLimit,就會直接報堆溢出異常
-XX:PermSize=10M -XX:MaxPermSize=10M -Xmx10M -Xms10M -XX:-UseGCOverheadLimit
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.Integer.toString(Integer.java:401)
at java.lang.String.valueOf(String.java:3099)
at org.example.App.main(App.java:13)
說明,常量池分配在堆中。
元數據區異常
JDK1.8之后,元數據區被放在了直接內存中,可以指定下面的參數來模擬溢出情況
- JVM參數:
- -XX:MetaspaceSize=10M
- -XX:MaxMetaspaceSize=10M
- -XX:+HeapDumpOnOutOfMemoryError
- 代碼:通過使用cglib生成大量的動態類
代碼實戰:
pom文件中添加cglib的引用
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.4</version>
</dependency>
package org.example;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class App {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
運行結果:
java.lang.OutOfMemoryError: Metaspace
Dumping heap to java_pid26272.hprof ...
Heap dump file created [3395669 bytes in 0.015 secs]
Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
at org.example.App.main(App.java:23)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:413)
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)
... 6 more
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
... 11 more
案例心得
- 元數據區和方法區錯誤的識別:java.lang.OutOfMemoryError: Metaspace;java.lang.OutOfMemoryError: PermGen space
- 元數據區的溢出一般和框架代碼或者本地代碼中大量創建動態類有關
- 核查問題時,也是根據具體的問題分析是哪個動態類被大量創建,是否有必要,是否需要調整方法區的大小。
直接內存區域的溢出
直接內存區域,如果內存達到設置的MaxDirectMemorySize后,就會觸發垃圾回收,如果垃圾回收不能有效回收內存,也會引起OOM溢出。
如何復現?
- JVM參數:-XX:MaxDirectMemorySize,如果不指定,和-Xmx指定的最大堆一樣
- 代碼部分:使用unsafe不斷的分配直接內存
代碼實戰
package org.example;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class App {
public static void main(String[] args) throws IllegalAccessException {
Field unsafeFiled = Unsafe.class.getDeclaredFields()[0];
unsafeFiled.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeFiled.get(null);
while (true) {
unsafe.allocateMemory(1024 * 1024);
}
}
}
運行結果
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.example.App.main(App.java:15)
案例心得:
- 直接內存溢出的識別:Exception in thread "main" java.lang.OutOfMemoryError,並且dump出的堆棧文件不大
- 核查問題時,根據異常堆棧檢查引發error的代碼,一般都是NIO代碼引起的。
實踐案例
如何正確利用大內存-高性能硬件上的程序部署策略
高性能硬件程序部署主要有兩種方式:
- 通過64位JDK使用來大內存
- 使用若干個32位的虛擬機建立邏輯集群以利用硬件資源
如果程序是對響應時間敏感的系統,想配置大堆的前提是,要保證應用的Full GC頻率足夠低,不會影響用戶的使用,比如,可以設置深夜定時任務觸發full-gc甚至自動重啟應用服務器來保證可用空間在一個穩定的水平。控制Full GC頻率的關鍵就是保證大多數對象都是朝生夕滅,不會短時間有大量對象進入老年代,引起頻繁的FullGC。
不僅如此,還需要考慮大堆帶來的其他問題:
- 內存回收時間變長
- 如果出現OOM,因為堆過大,幾乎無法分析dump文件
- 64位的JDK性能測試結果要弱於32位的JDK,並且占用內存也較大
所以建議,如非必要,盡可能使用第二種方式來部署以充分利用高性能硬件資源。
第二種方式就是集群部署方式,采用集群部署就需要考慮額外的問題,比如,如何保留用戶的狀態,一般有兩種解決方案:
-
親和式集群:同一個用戶都轉發到同一個服務器去處理
-
優點:實現簡單,只需要在apache等負載均衡器中配置即可;網絡流量較少,客戶端只需要傳遞sessionID即可
-
缺點:用戶和對應服務器綁定,一旦服務器宕機,用戶的session狀態即消失
-
apache中這樣配置:
worker.controller.sticky_session=true|false true為親和式集群 worker.controller.sticky_session_force=true|false false為當服務器不可用,轉發到其他服務器
-
-
共享session:集群內服務器共享session
- 優點:服務器宕機,用戶也不會丟失session狀態
- 缺點:在系統中引入了新的組件,提高了系統的復雜度,實現復雜度
針對第二種方式,和第一種方式對比,也有自己的缺點,我們在設計系統機構時也需要考慮到:
- 同一台物理機器的上的資源競爭(並發寫),首先會想到可以使用同步機制,可以學習鎖設計中的分段鎖和間隙鎖,通過鎖一部分來提高並發度;或者通過樂觀鎖的設計,不斷循環更新直到成功;還可以考慮建立熱訪問資源,提前把一部分資源緩存到集中緩存中,通過集中式緩存減少磁盤IO競爭。
- 冗余的本地內存,可以考慮使用集中式內存數據庫解決
- 資源池浪費,可以考慮使用JNDI(統一命名服務,我覺得和Springcloud中的統一配置中心核心思想是一致的,都是把配置文件統一放在一個地方,便於引用維護),但是也會帶來新的復雜度
總結:
- 高性能硬件的部署策略有兩種,考慮到GC時間過長,堆轉出日志無法分析等缺點,盡量選擇多實例部署的邏輯集群方式
- 邏輯集群的部署方式要考慮 狀態保持、資源競爭和資源冗余等情況,根據具體業務場景靈活應用。
如何排查內存溢出錯誤
堆外內存溢出一般主要來源於操作系統對進程的內存限制 和 堆外內存回收的機制。
針對操作系統堆進程的內存限制。比如:32位的windows操作系統對進程的限制為2G,如果堆等其他區域划分的內存過大,那么留給直接內存區域的內存就非常小了。
針對堆外內存的回收機制。堆外內存需要等到滿了之后,再在代碼中觸發System.gc來回收,如果服務器開啟-XX:+DisableExplicitGC參數開關,那么就不會響應這次垃圾回收的請求。
總結:
因為限制以及其他區域不合理的參數配置,直接內存區域只有很小的一塊內存;並且垃圾回收需要依靠手動觸發System.gc來回收無法保證回收的可靠性,所以溢出就是必然的了。
我這里又查閱了之前看過印象深刻的一個關於美團使用網絡框架的一個堆外內存泄漏bug。這里給大家簡單介紹下,原文詳見這里:Netty堆外內存泄露排查盛宴
首先作者通過nginx不斷報5XX異常發現服務不可用,然后核查jvm發現頻繁的fullgc導致用戶線程阻塞(其實就是netty的nio線程),最后查出是log4j2在某個時點大量頻繁的打印堆外內存不足的error日志導致的,所以這個問題的核心在於排查堆外內存為何泄漏。
排查的步驟首先是基於異常的堆棧日志,找到對應的代碼,用反射機制每隔N秒觀察堆外內存的大小,發現了堆外內存增長的規律。然后猜測原因,模擬測試查看是否可以復現問題,成功復現問題后,就能大約找到出現問題的代碼,繼續通過debug查找根源代碼處,最終通過修改代碼,重新build后最終解決問題。
我個人認為這個問題解決的關鍵在於開發者能夠讀懂框架自己使用變量統計堆外內存,然后得以跟蹤這個變量最終解決問題。我們在排查問題的時候如果也可以多想一些,多去琢磨框架報出異常的原因,也許就能找到解決問題的辦法。
如何排查系統CPU性能指標異常-外部命令導致系統緩慢
案例介紹:在做壓力測試時發現系統的CPU指標異常,大量的時間被系統調用fork占用,最后核查代碼發現這個fork系統調用是在每一個請求來臨時,都會調用以獲取系統相關的信息的,具體是使用Runtime.getRuntime().exec()來執行一段shell腳本,最后修改為通過java api調用,問題解決。
案例收獲:
- cpu等系統指標異常,一般都是來源於應用代碼的某些操作,需要仔細檢查代碼中會導致系統調用的部分,采用其他替代方式實現。
- java中獲取操作系統信息,其實是一個很常見的操作,可以通過java api實現。https://www.cnblogs.com/HopkinsCybn/p/10055964.html