SpringBoot中排查內(nèi)存泄漏的方法小結(jié)
一、引言
內(nèi)存泄漏是項(xiàng)目開(kāi)發(fā)中常見(jiàn)且棘手的問(wèn)題,它會(huì)導(dǎo)致應(yīng)用性能下降、響應(yīng)變慢,嚴(yán)重時(shí)甚至?xí)l(fā)OutOfMemoryError異常導(dǎo)致應(yīng)用崩潰。
與傳統(tǒng)的Java應(yīng)用相比,SpringBoot應(yīng)用因其豐富的組件生態(tài)和依賴(lài)注入的特性,內(nèi)存泄漏問(wèn)題可能更加隱蔽和復(fù)雜。
本文將介紹多種實(shí)用的方法來(lái)排查應(yīng)用中的內(nèi)存泄漏問(wèn)題。
二、內(nèi)存泄漏基礎(chǔ)知識(shí)
在深入排查方法之前,先簡(jiǎn)單回顧一下內(nèi)存泄漏的基本概念:
內(nèi)存泄漏(Memory Leak) :程序分配的內(nèi)存由于某種原因無(wú)法被釋放,導(dǎo)致這部分內(nèi)存一直被占用,無(wú)法被GC回收。
在Java中,內(nèi)存泄漏通常表現(xiàn)為對(duì)象被引用但實(shí)際上不再需要,從而無(wú)法被垃圾回收器回收。
SpringBoot應(yīng)用中常見(jiàn)的內(nèi)存泄漏原因包括:
靜態(tài)集合類(lèi)引用:如靜態(tài)的Map、List持有對(duì)象引用
單例bean中的集合類(lèi)引用:Spring的單例bean生命周期與應(yīng)用一致
未關(guān)閉的資源:數(shù)據(jù)庫(kù)連接、文件流等
不當(dāng)?shù)木彺媸褂?/strong>:無(wú)界緩存或緩存過(guò)期策略設(shè)置不當(dāng)
線程池管理不當(dāng):任務(wù)隊(duì)列無(wú)限增長(zhǎng)
JNI調(diào)用未釋放的本地內(nèi)存
類(lèi)加載器泄漏:如WebappClassLoader在熱部署時(shí)未釋放
三、內(nèi)存泄漏排查方法
1. JVM啟動(dòng)參數(shù)配置與GC日志分析
通過(guò)配置適當(dāng)?shù)腏VM參數(shù),可以記錄詳細(xì)的GC日志,幫助分析內(nèi)存使用情況。
實(shí)施步驟:
添加以下JVM參數(shù)啟用GC日志:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:/path/to/gc.log
在SpringBoot應(yīng)用中,可以在application.properties中配置:
spring.jvm.args=-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log
使用GCViewer等工具分析GC日志,關(guān)注以下指標(biāo):
- Full GC頻率異常增高
- GC后內(nèi)存回收效果不明顯
- 老年代內(nèi)存持續(xù)增長(zhǎng)
示例GC日志片段分析:
2023-08-10T14:15:30.245+0800: [GC (Allocation Failure) [PSYoungGen: 786432K->9437K(917504K)] 786432K->9445K(3014656K), 0.0088311 secs]
2023-08-10T14:16:30.377+0800: [GC (Allocation Failure) [PSYoungGen: 795869K->8941K(917504K)] 795877K->23757K(3014656K), 0.0102321 secs]
2023-08-10T14:17:30.502+0800: [GC (Allocation Failure) [PSYoungGen: 795373K->10022K(917504K)] 810189K->54038K(3014656K), 0.0143901 secs]
注意觀察GC后內(nèi)存占用持續(xù)增長(zhǎng)的趨勢(shì),這可能表明存在內(nèi)存泄漏。
2. 使用JConsole實(shí)時(shí)監(jiān)控
JConsole是JDK自帶的圖形化監(jiān)控工具,可以實(shí)時(shí)監(jiān)控JVM內(nèi)存、線程和類(lèi)加載情況。
實(shí)施步驟:
啟動(dòng)SpringBoot應(yīng)用時(shí)添加JMX參數(shù):
-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false
運(yùn)行JConsole:jconsole命令或從JDK的bin目錄啟動(dòng)
連接到目標(biāo)應(yīng)用,觀察"內(nèi)存"選項(xiàng)卡,特別關(guān)注以下區(qū)域:
- 堆內(nèi)存使用趨勢(shì)(持續(xù)上升表明可能存在問(wèn)題)
- 永久代/元空間使用情況
- GC活動(dòng)頻率
在"MBeans"選項(xiàng)卡中,可以查看Spring相關(guān)的Bean信息
3. VisualVM進(jìn)行高級(jí)內(nèi)存分析
VisualVM是一個(gè)功能更強(qiáng)大的分析工具,可以生成堆轉(zhuǎn)儲(chǔ)并分析內(nèi)存使用情況。
實(shí)施步驟:
- 下載并啟動(dòng)VisualVM(JDK 8之前自帶,之后需單獨(dú)下載)
- 連接到目標(biāo)應(yīng)用,在"應(yīng)用程序"視圖中選擇你的應(yīng)用
- 在"監(jiān)視"選項(xiàng)卡觀察內(nèi)存使用趨勢(shì)
- 使用"堆轉(zhuǎn)儲(chǔ)"按鈕創(chuàng)建堆轉(zhuǎn)儲(chǔ)文件
- 在"類(lèi)"視圖中,按實(shí)例數(shù)量排序,查找異常增多的對(duì)象
- 檢查可疑對(duì)象的引用鏈,找出引用源
分析技巧:
- 對(duì)比多個(gè)時(shí)間點(diǎn)的堆轉(zhuǎn)儲(chǔ),觀察哪些對(duì)象數(shù)量異常增長(zhǎng)
- 使用SQL(對(duì)象查詢(xún)語(yǔ)言)進(jìn)行高級(jí)查詢(xún)
SELECT s FROM java.util.HashMap s WHERE s.size > 1000
4. MAT(Memory Analyzer Tool)詳細(xì)堆分析
Eclipse Memory Analyzer是專(zhuān)門(mén)用于分析Java堆轉(zhuǎn)儲(chǔ)文件的工具,能夠找出潛在的內(nèi)存泄漏。
實(shí)施步驟:
獲取堆轉(zhuǎn)儲(chǔ)文件(可以使用VisualVM或jmap命令):
jmap -dump:format=b,file=heap.hprof <PID>
使用MAT打開(kāi)堆轉(zhuǎn)儲(chǔ)文件
運(yùn)行"Leak Suspects Report",自動(dòng)分析可能的內(nèi)存泄漏
使用"Dominator Tree"查看占用內(nèi)存最多的對(duì)象
檢查可疑對(duì)象的GC Roots和引用鏈
分析關(guān)鍵點(diǎn):
- 關(guān)注"Retained Heap"列,它表示對(duì)象及其引用的所有對(duì)象占用的總內(nèi)存
- 使用"Path to GC Roots"查找阻止對(duì)象被回收的引用路徑
- 檢查集合類(lèi)(如HashMap、ArrayList)中的元素
5. 使用Spring Boot Actuator監(jiān)控
Spring Boot Actuator提供了豐富的監(jiān)控端點(diǎn),可以用來(lái)監(jiān)控應(yīng)用內(nèi)存使用情況。
實(shí)施步驟:
添加Actuator依賴(lài):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在application.properties中開(kāi)啟相關(guān)端點(diǎn):
management.endpoints.web.exposure.include=health,metrics,heapdump management.endpoint.health.show-details=always
訪問(wèn)指標(biāo)端點(diǎn)查看內(nèi)存使用情況:
/actuator/metrics/jvm.memory.used- 查看內(nèi)存使用/actuator/metrics/jvm.gc.memory.promoted- 查看提升到老年代的內(nèi)存/actuator/heapdump- 下載堆轉(zhuǎn)儲(chǔ)文件
可以集成Prometheus和Grafana進(jìn)行長(zhǎng)期監(jiān)控和告警
示例代碼 - 自定義內(nèi)存監(jiān)控端點(diǎn):
@Component
@Endpoint(id = "memory-status")
public class MemoryStatusEndpoint {
@ReadOperation
public Map<String, Object> memoryStatus() {
Map<String, Object> status = new HashMap<>();
Runtime runtime = Runtime.getRuntime();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long maxMemory = runtime.maxMemory();
long usedMemory = totalMemory - freeMemory;
status.put("total", bytesToMB(totalMemory));
status.put("free", bytesToMB(freeMemory));
status.put("used", bytesToMB(usedMemory));
status.put("max", bytesToMB(maxMemory));
status.put("usagePercentage", usedMemory * 100.0 / maxMemory);
return status;
}
private double bytesToMB(long bytes) {
return bytes / (1024.0 * 1024.0);
}
}
6. 使用jstack分析線程堆棧
線程相關(guān)問(wèn)題也可能導(dǎo)致內(nèi)存泄漏,如線程池使用不當(dāng)或線程持有大對(duì)象引用。
實(shí)施步驟:
使用jstack命令獲取線程轉(zhuǎn)儲(chǔ):
jstack <PID> > thread_dump.txt
分析線程狀態(tài),關(guān)注以下點(diǎn):
- 大量BLOCKED狀態(tài)的線程(可能表明有死鎖)
- 線程數(shù)量異常增多(可能有線程泄漏)
- 線程堆棧深度異常(可能有遞歸或循環(huán)依賴(lài))
結(jié)合jmap查看每個(gè)線程的內(nèi)存占用:
jmap -histo:live <PID> | head -20
分析示例:
"http-nio-8080-exec-10" #43 daemon prio=5 os_prio=0 tid=0x00007f8b5c34e800 nid=0x7f82 waiting on condition [0x00007f8a3bf7c000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
7. 使用YourKit等商業(yè)工具進(jìn)行全面分析
YourKit、JProfiler等商業(yè)工具提供了更全面的內(nèi)存分析功能。
實(shí)施步驟:
- 安裝YourKit Java Profiler并配置應(yīng)用連接
- 使用"內(nèi)存"視圖實(shí)時(shí)監(jiān)控內(nèi)存使用情況
- 創(chuàng)建多個(gè)堆快照進(jìn)行對(duì)比分析
- 使用"對(duì)象計(jì)數(shù)"功能查看不同類(lèi)型對(duì)象的數(shù)量變化
- 設(shè)置對(duì)象創(chuàng)建跟蹤,找出創(chuàng)建大量對(duì)象的代碼
特別功能:
- 內(nèi)存泄漏檢測(cè)器自動(dòng)分析可能的泄漏
- 可以捕獲具體的內(nèi)存分配點(diǎn)(allocation points)
- 支持查看保留的內(nèi)存分布
8. 數(shù)據(jù)庫(kù)連接與資源泄漏檢測(cè)
數(shù)據(jù)庫(kù)連接、文件句柄等資源未正確關(guān)閉是常見(jiàn)的泄漏源。
實(shí)施步驟:
使用數(shù)據(jù)庫(kù)連接池監(jiān)控功能,如HikariCP的指標(biāo):
spring.datasource.hikari.register-mbeans=true
通過(guò)JMX查看連接池狀態(tài):
- 活躍連接數(shù)
- 等待連接數(shù)
- 總連接數(shù)
代碼審查,確保所有資源都在try-with-resources塊中使用:
// 正確方式
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users");
ResultSet rs = ps.executeQuery()) {
// 處理結(jié)果集
} catch (SQLException e) {
logger.error("Database error", e);
}
// 錯(cuò)誤方式 - 可能導(dǎo)致連接泄漏
Connection conn = null;
try {
conn = dataSource.getConnection();
// ...如果這里拋出異常,連接可能不會(huì)關(guān)閉
} finally {
// 可能遺漏關(guān)閉或異常處理不當(dāng)
}
使用lsof命令檢查進(jìn)程打開(kāi)的文件句柄數(shù):
lsof -p <PID> | wc -l
9. 使用BTrace進(jìn)行運(yùn)行時(shí)分析
BTrace是一個(gè)強(qiáng)大的Java運(yùn)行時(shí)跟蹤工具,可以在不重啟應(yīng)用的情況下動(dòng)態(tài)分析對(duì)象創(chuàng)建和方法調(diào)用。
實(shí)施步驟:
下載安裝 BTrace
編寫(xiě)B(tài)Trace腳本跟蹤可疑方法:
import org.openjdk.btrace.core.annotations.*;
import static org.openjdk.btrace.core.BTraceUtils.*;
@BTrace
public class MemoryLeakTracer {
@OnMethod(
clazz="com.example.service.CacheService",
method="addToCache"
)
public static void traceAdd(@Self Object self, @ProbeClassName String pcn, @ProbeMethodName String pmn, Object key, Object value) {
println("Adding to cache: " + str(key));
println("Cache size: " + get(field(classOf("com.example.service.CacheService"), "cache", self), "size"));
}
}
將腳本附加到運(yùn)行中的應(yīng)用:
btrace <PID> MemoryLeakTracer.java
分析輸出,尋找異常增長(zhǎng)的集合或頻繁創(chuàng)建的大對(duì)象
10. 代碼審查常見(jiàn)內(nèi)存泄漏模式
系統(tǒng)地審查代碼中的常見(jiàn)內(nèi)存泄漏模式可以有效預(yù)防問(wèn)題。
需關(guān)注的模式:
1. 靜態(tài)集合:
public class EventCollector {
// 危險(xiǎn):無(wú)界靜態(tài)集合
private static final List<Event> ALL_EVENTS = new ArrayList<>();
public void recordEvent(Event event) {
ALL_EVENTS.add(event); // 不斷增長(zhǎng),從不清理
}
}
2. 未關(guān)閉的資源:
public byte[] readFile(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
// 錯(cuò)誤:未使用try-with-resources,可能導(dǎo)致文件句柄泄漏
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int data;
while ((data = fis.read()) != -1) {
buffer.write(data);
}
// fis未關(guān)閉!
return buffer.toByteArray();
}
3. 內(nèi)部類(lèi)引用:
public class Outer {
private final byte[] largeArray = new byte[10 * 1024 * 1024];
public Runnable createTask() {
// 非靜態(tài)內(nèi)部類(lèi)持有外部類(lèi)引用,可能導(dǎo)致largeArray無(wú)法釋放
return new Runnable() {
@Override
public void run() {
System.out.println("Task running");
}
};
}
}
4. 緩存使用不當(dāng):
@Service
public class ProductService {
// 不限大小的緩存,沒(méi)有過(guò)期策略
private final Map<String, Product> productCache = new HashMap<>();
public Product getProduct(String id) {
if (!productCache.containsKey(id)) {
Product product = repository.findById(id);
productCache.put(id, product); // 持續(xù)增長(zhǎng)
}
return productCache.get(id);
}
}
5. 線程池配置不當(dāng):
// 無(wú)界隊(duì)列可能導(dǎo)致內(nèi)存溢出
ExecutorService executor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>() // 無(wú)界隊(duì)列
);
11. 壓力測(cè)試暴露內(nèi)存問(wèn)題
通過(guò)壓力測(cè)試可以更快地暴露內(nèi)存泄漏問(wèn)題。
實(shí)施步驟:
- 使用JMeter或Gatling創(chuàng)建測(cè)試腳本,模擬真實(shí)業(yè)務(wù)場(chǎng)景
- 設(shè)置循環(huán)執(zhí)行測(cè)試用例,持續(xù)觀察內(nèi)存使用趨勢(shì)
- 監(jiān)控GC活動(dòng)和內(nèi)存分配情況
- 增加負(fù)載直到發(fā)現(xiàn)異常內(nèi)存增長(zhǎng)
- 獲取堆轉(zhuǎn)儲(chǔ)進(jìn)行分析
壓測(cè)注意事項(xiàng):
- 逐步增加并發(fā)用戶數(shù),避免立即施加高負(fù)載
- 測(cè)試周期應(yīng)足夠長(zhǎng),某些內(nèi)存泄漏可能需要長(zhǎng)時(shí)間積累才顯現(xiàn)
- 關(guān)注不同業(yè)務(wù)場(chǎng)景的內(nèi)存使用差異
- 每次測(cè)試前重啟應(yīng)用,確?;€一致
五、預(yù)防內(nèi)存泄漏的最佳實(shí)踐
1. 集合類(lèi)使用注意事項(xiàng)
- 優(yōu)先使用有界集合,如
ArrayBlockingQueue而非無(wú)界的LinkedBlockingQueue - 使用
WeakHashMap存儲(chǔ)可緩存但不必須的對(duì)象 - 定期檢查和清理長(zhǎng)期存活的集合
2. 資源管理規(guī)范
- 始終使用try-with-resources關(guān)閉IO資源
- 實(shí)現(xiàn)
AutoCloseable接口并在@PreDestroy方法中清理資源 - 使用連接池監(jiān)控功能,設(shè)置合理的最大連接數(shù)和超時(shí)時(shí)間
3. 緩存使用策略
- 使用專(zhuān)業(yè)緩存框架如Caffeine或Ehcache,而非自定義Map
- 設(shè)置適當(dāng)?shù)木彺娲笮∩舷藓瓦^(guò)期策略
- 考慮使用弱引用或軟引用緩存非關(guān)鍵數(shù)據(jù)
4. 開(kāi)發(fā)階段內(nèi)存檢測(cè)
- 在開(kāi)發(fā)和測(cè)試環(huán)境使用較小的堆內(nèi)存,更快暴露問(wèn)題
- 編寫(xiě)單元測(cè)試驗(yàn)證資源釋放
- 使用FindBugs或SpotBugs等靜態(tài)分析工具檢測(cè)潛在問(wèn)題
5. 生產(chǎn)環(huán)境監(jiān)控策略
- 配置內(nèi)存使用告警
- 定期采集和分析GC日志
- 自動(dòng)化生成周期性堆轉(zhuǎn)儲(chǔ)并分析
六、總結(jié)
內(nèi)存泄漏問(wèn)題是Java應(yīng)用尤其是長(zhǎng)期運(yùn)行的SpringBoot應(yīng)用面臨的常見(jiàn)挑戰(zhàn)。在實(shí)際應(yīng)用中,通常需要結(jié)合多種方法進(jìn)行綜合分析,才能準(zhǔn)確找出問(wèn)題根源。
同時(shí),完善的監(jiān)控體系也能幫助我們及早發(fā)現(xiàn)并解決潛在問(wèn)題,確保應(yīng)用的長(zhǎng)期穩(wěn)定運(yùn)行。
以上就是SpringBoot中排查內(nèi)存泄漏的方法小結(jié)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot內(nèi)存泄漏排查的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java中如何使用BufferedImage判斷圖像通道順序并轉(zhuǎn)RGB/BGR
這篇文章主要介紹了java中如何BufferedImage判斷圖像通道順序并轉(zhuǎn)RGB/BGR的相關(guān)資料,需要的朋友可以參考下2017-03-03
java中的equals()和toString()方法實(shí)例詳解
這篇文章主要介紹了java中的equals()和toString()方法實(shí)例詳解的相關(guān)資料,這里舉例說(shuō)明,并附實(shí)例代碼,和實(shí)現(xiàn)效果圖,需要的朋友可以參考下2016-11-11
MyBatis SqlSession 與緩存機(jī)制實(shí)例詳解
Spring整合MyBatis中的SqlSession管理機(jī)制與緩存策略,詳細(xì)解析了SqlSession綁定線程、事務(wù)的原理,以及一級(jí)、二級(jí)緩存的工作機(jī)制與配置方法,感興趣的朋友跟隨小編一起看看吧2026-06-06
Java關(guān)系操作符簡(jiǎn)寫(xiě)介紹
下面小編就為大家?guī)?lái)一篇Java關(guān)系操作符簡(jiǎn)寫(xiě)介紹。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-05-05
ThreadPoolExecutor參數(shù)的用法及說(shuō)明
這篇文章主要介紹了ThreadPoolExecutor參數(shù)的用法及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
刪除IDEA中的JDK列表項(xiàng)實(shí)現(xiàn)方式
解決IDEA中顯示已卸載JDK的問(wèn)題,通過(guò)項(xiàng)目結(jié)構(gòu)管理SDK并刪除不需要的版本,可能需要多次嘗試才能完全移除2026-01-01
Java實(shí)現(xiàn)讀取Word模板文檔并替換內(nèi)容生成新文檔
在實(shí)際開(kāi)發(fā)中,經(jīng)常會(huì)遇到需要根據(jù) Word 模板生成特定文檔的需求,下面小編就來(lái)為大家介紹一下如何使用 Apache POI 庫(kù)來(lái)讀取 Word 模板文檔,然后替換其中的指定內(nèi)容最后生成新的文檔吧2025-02-02

