前言
有些人傻傻分不清記憶體泄漏和記憶體溢位的區別,這里簡單做個科普
- 記憶體溢位:就是記憶體不夠用了,物件需要的記憶體大小大于你分配的堆大小,記憶體溢位最常見的錯誤就是
OutOfMemoryError,簡稱OOM; - 記憶體泄漏:物件用完之后沒被垃圾回收器(GC)回收,既然沒被回收,那么這個物件就會一直占用著記憶體空間,這就是記憶體泄漏,記憶體泄漏的最終結果就是會導致記憶體溢位,因為物件一直占用,久而久之,一直疊加到超過最大堆記憶體時,就會導致OOM,
本次分析記憶體泄漏的工具主要有2個,一個是arthas,另一個是jdk自帶的工具jmap,關于這2個工具的用法,可以參考我之前寫的2篇文章:
- arthas : Arthas使用教程 阿里巴巴開源專案、史上最強java線上診斷工具
- jmap: 原來jdk自帶了這么好玩的工具 > jmap 使用教程
模擬記憶體泄漏
下列的java代碼是一個模擬線上的記憶體泄漏的代碼,這段代碼的業務邏輯是從資料庫中讀取信用資料,套用模型,并把結果進行記錄和傳輸;
package com.gc;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 從資料庫中讀取信用資料,套用模型,并把結果進行記錄和傳輸
*
* 啟動時加入以下引數 : -Xms200M -Xmx200M -XX:+UseParallelGC -XX:+PrintGC -XX:+HeapDumpOnOutOfMemoryError
* 發現啟動后會頻繁GC,最后導致OOM(OutOfMemoryError)
*/
public class T15_FullGC_Problem01 {
private static class CardInfo {
BigDecimal price = new BigDecimal(0.0);
String name = "張三";
int age = 5;
Date birthdate = new Date();
public void m() {}
}
private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
new ThreadPoolExecutor.DiscardOldestPolicy());
/**
* main方法
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
executor.setMaximumPoolSize(50);
// 為什么是死回圈?因為在生產環境中會有源源不斷的資料需要處理,我們無法模擬線上環境, 所以用死回圈代替;
for (;;){
modelFit();
Thread.sleep(100);
}
}
private static void modelFit(){
List<CardInfo> taskList = getAllCardInfo();
taskList.forEach(info -> {
// do something
executor.scheduleWithFixedDelay(() -> {
//do sth with info
info.m();
}, 2, 3, TimeUnit.SECONDS);
});
}
private static List<CardInfo> getAllCardInfo(){
List<CardInfo> taskList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
CardInfo ci = new CardInfo();
taskList.add(ci);
}
return taskList;
}
}
啟動
啟動時加入引數-Xms200M -Xmx200M -XX:+UseParallelGC -XX:+PrintGC -XX:+HeapDumpOnOutOfMemoryError,這段代碼運行后,老年代的記憶體占用會慢慢升高,待記憶體占用到達頂峰時,會頻繁Full GC(回收老年代垃圾),直到撐爆記憶體,最后會導致OOM例外:Exception in thread "pool-1-thread-1" java.lang.OutOfMemoryError: GC overhead limit exceeded;也就是記憶體溢位;
運行監控
運行一段時間后,可以看到一直不停地full GC,并且有些執行緒的記憶體已經溢位報出OOM錯誤;

解決方案:使用arthas
在用arthas 的dashboard命令看一下記憶體使用情況,這一看,我的天,老年代的記憶體已經使用了98.64%,并且已經進行了3914次的GC,也就是說Full GC進行了3914次清理都沒清掉那些垃圾;

到這時候我們就已經確定發現了記憶體泄漏,接下來的作業就是要找到是那些頑固的物件沒被清理調,然后在做出相應的調整;
首先要分析堆記憶體有哪些物件,這里使用到一個arthas的工具:heapdump,這個命令類似jdk的jmap,使用arthas匯出堆轉儲檔案命令;
[arthas@28747]$ heapdump --live /Users/mac/Downloads/dump.hprof
Dumping heap to /Users/mac/Downloads/dump.hprof ...
Heap dump file created
匯出后是一個二進制檔案,這個檔案直接打開看到的是亂碼的,所以我們需要借助一些工具,這邊有2個選擇,用jhat 和 jvisualVM,因為jhat用的不多,所以我們用大家常用的jvisualVM.
使用jvisual VM加載堆轉儲檔案dump.hprof后如下圖:

由此結果可以看到,Date物件和Bigdecimal物件一直無法回收,而每一個定時任務就會創建一個Date物件和Bigdecimal物件,到現在為止已經有55萬個了,因為只增不減,撐爆記憶體是遲早的事,
解決方案二:使用jmap
先使用jps命令找到正在運行的java行程id
macdeMacBook-Pro:Downloads mac$ jps
24240 App
29410 Launcher
29411 T15_FullGC_Problem01
2851
29414 Jps
29031 Main
我們運行的類為T15_FullGC_Problem01,對應的行程id為29411,記住這個行程id;
接著用jmap命令查看這個行程id,看看記憶體占用排行前十的物件有哪些:
jmap -histo:live 29411| head -10
-histo:live:表示只查看存活的物件head - 10:這是linux自帶的命令,表示查看頭部前十行內容;
運行后,結果和上面的jvisual分析結果差不多,Date物件和Bigdecimal物件都是在CardInfo方法里面的,所以排行最高的就是這三個;
macdeMacBook-Pro:Downloads mac$ jmap -histo:live 29411| head -10
num #instances #bytes class name
----------------------------------------------
1: 447100 32191200 java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask
2: 447126 17885040 java.math.BigDecimal
3: 447100 14307200 com.gc.T15_FullGC_Problem01$CardInfo
4: 447100 10730400 java.util.Date
5: 447100 10730400 java.util.concurrent.Executors$RunnableAdapter
6: 447100 7153600 com.gc.T15_FullGC_Problem01$$Lambda$2/664223387
7: 1 2396432 [Ljava.util.concurrent.RunnableScheduledFuture;
為什么Date和Bigdecimal物件沒被回收
是因為modelFit()方法出現了問題,
private static void modelFit(){
List<CardInfo> taskList = getAllCardInfo();
taskList.forEach(info -> {
// do something
executor.scheduleWithFixedDelay(() -> {
//do sth with info
info.m();
}, 2, 3, TimeUnit.SECONDS);
});
}
仔細看這個方法內的代碼,關于這段代碼可以有2種解釋,
1、taskList鏈接著info物件
info是taskList中的元素,每個info元素都taskList這個物件所參考著,每次定時任務執行完后,執行緒內的物件都會被垃圾回收器清理掉,但是info這個物件不屬于定時任務執行緒內的物件,所以沒被清理掉;按理說taskList內的所有物件都遍歷完了之后,應該會將taskList給清除掉,但是taskList還有個別元素在執行緒中,他們之間的參考還在,既然有參考,也就自然不會被清理;參考關系如下圖
2、執行緒參考這info物件
info是taskList中的元素,每個info元素都taskList這個物件所參考著,這個定時任務executor.scheduleWithFixedDelay(() -> { }); 花括號的內容其實是在另一個域里面了,雖然info.m()這個方法已經執行完了,但是執行完后執行緒并沒有被回收,因為執行緒的核心執行緒數和最大執行緒數都設定為50,所以執行緒執行完后一直在那掛著,既然執行緒還沒回收,執行緒中GC Roots對info物件的參考就一直在,既然參考還在,垃圾回收器就不會回收被參考的物件;參考關系如下圖

解決方案
要解決這個問題,就得解決參考的問題,讓物件參考隨著執行緒的執行完畢而清理掉,所以只需要修改modelFit()方法為以下代碼即可解決問題,以下這段代碼,taskList 在執行緒內執行,一旦執行緒執行完后taskList也會隨著執行緒一起被回收掉,另外taskList內的所有CardInfo也會被回收,緊接著Date和Bigdecimal物件就沒有參考了,也會被垃圾回收器回收掉,到這里也就解決記憶體泄漏問題,
private static void modelFit(){
executor.scheduleWithFixedDelay(() -> {
List<CardInfo> taskList = getAllCardInfo();
taskList.forEach(info -> {
//do sth with info
info.m();
});
}, 2, 3, TimeUnit.SECONDS);
}
完
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/289858.html
標籤:java
上一篇:SpringBoot個人總結
下一篇:面試官:說一下類加載的程序
