上一次講到內(nèi)存監(jiān)控的幾個點,這里來分析但發(fā)生內(nèi)存問題時dump的時機及方式
dump的時機
1.activity(fragment)生命周期結(jié)束
??在 activity 調(diào)用 onDestory 之后,可以認(rèn)為該 activity 及與之相關(guān)的東西都沒用了,應(yīng)該被回收。所以可以在調(diào)用 onDestory 之后將 activity 放入一個若引用中,隔一段時間來查看弱引用中的 activity 是否還存在來判斷 activity 是否被系統(tǒng)回收。
注意點
- 隔一段時間是因為當(dāng)調(diào)用 Runtime.getRuntime().gc(); System.runFinalization(); 之類的函數(shù),系統(tǒng)也不會立即就去執(zhí)行回收操作,還是要看系統(tǒng)何時去執(zhí)行 GC 操作。
- 判斷若引用內(nèi)的 obj 是否被回收有二種方法,一是判斷 WeakReference.get() == null ,二是在構(gòu)造 WeakReference 時傳入一個 ReferenceQueue 隊列,當(dāng)弱引用內(nèi)的 obj 被回收時,隊列里面就可以取出東西來判斷。
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks {
...
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
CheckLeakMomory.getInstance().addMoniter(activity);
}
});
public class CheckLeakMomory implements Runnable {
private volatile static CheckLeakMomory instance;
private final long gcTime = 5_000;
private List<ActivityInfo> activityList = new ArrayList();
private ReferenceQueue queue;
private HandlerThread handlerThread;
private Handler handler;
private CheckLeakMomory() {
queue = new ReferenceQueue();
handlerThread = new HandlerThread("CheckLeakMomory");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
public static CheckLeakMomory getInstance() {
if (instance == null) {
synchronized (CheckLeakMomory.class) {
if (instance == null) {
instance = new CheckLeakMomory();
}
}
}
return instance;
}
public void addMoniter(Activity activity) {
WeakReference<Activity> activityWeakReference = new WeakReference(activity, queue);
activityList.add(0, new ActivityInfo(activityWeakReference));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (!handler.hasCallbacks(this)) {
handler.postDelayed(this, gcTime);
}
}
}
@Override
public void run() {
runGC();
boolean hasnext = false;
for (ActivityInfo info : activityList) {
if (info.isLeak()) {
dumpMemory(info.getActivity());
clear();
break;
} else if (info.waitNextGC()) {
hasnext = true;
}
}
if (hasnext) {
runGC();
runGC();
runGC();
handler.postDelayed(this, gcTime);
}
}
private void runGC() {
Runtime.getRuntime().gc();
System.runFinalization();
}
private void clear() {
activityList.clear();
handler.removeCallbacks(this);
}
private void dumpMemory(Activity activity) {
Log.e("lyll", "發(fā)現(xiàn)內(nèi)存蟹肉 " + activity.getClass().getSimpleName());
try {
String path =new File(activity.getFilesDir().getPath()).getAbsolutePath();
HeapAnalyzer.getInstance().dumpAndAnalysisLeak(path,activity.getClass().getSimpleName());
} catch (Exception e) {
e.printStackTrace();
}
}
class ActivityInfo {
private WeakReference<Activity> activityWeakReference;
private int gcTimes = 0;
public ActivityInfo(WeakReference<Activity> activityWeakReference) {
this.activityWeakReference = activityWeakReference;
}
public boolean waitNextGC() {
return gcTimes <= 3;
}
public boolean isLeak() {
gcTimes++;
if (activityWeakReference.get() != null && gcTimes > 3) {
return true;
}
return false;
}
public Activity getActivity() {
return activityWeakReference.get();
}
}
}
這里判斷 actviity 是否被回收判斷了3次,之前判斷一次時會出現(xiàn)極個別情況是可以被正?;厥盏牡窃谝淮窝訒r內(nèi)未被回收的情況,所以判斷了三次
2.內(nèi)存閾值的判斷
??每隔一段時間去輪詢判斷當(dāng)前進程的內(nèi)存使用比,線程數(shù),打開文件數(shù)來判斷當(dāng)前進程的內(nèi)存情況,優(yōu)點是可以統(tǒng)計除 activity 和 fragment 外的內(nèi)存使用,目前也沒有很多限制。
- 在 linux 中,打開的線程數(shù)和文件數(shù)是有限制的,好像是 1024 個(和使用的文件系統(tǒng)有關(guān))。在 android 中也是有限制,可以通過命令查看。
//查看最大線程數(shù)
cat /proc/sys/kernel/threads-max
//查看最大打開文件數(shù)
adb shell ulimit -a
cat /proc/pid/limits
當(dāng)打開超過閾值時,就會發(fā)生OOM。
代碼參考自 KOOM
public class KOOMMoniter extends Moniter implements Runnable {
private static final String TAG = "KOOMMoniter";
private final int CHECK_INTERVAL = 10_000;
private final float HEAP_RATIO_THRESHOLD_GAP = 0.05f;
private final int THREAD_COUNT_THRESHOLD_GAP = 50;//Thread連續(xù)值遞增浮動范圍50
private Application application;
private ILog logImpl;
private HandlerThread handlerThread;
private Handler handler;
//heap
private float heapThreshold = 0.8f;
private float mLastHeapRatio;
private int mOverThresholdCount;
private int heapMaxOverThresholdCount = 3;
//thread
private int threadThreshold = 200;
private int mLastThreadCount;
private int threadMaxOverThresholdCount = 600;
//fd
private int fdThreshold = 1000;
private int mLastFdCount;
private final int FD_COUNT_THRESHOLD_GAP = 50; //FD連續(xù)值遞增浮動范圍50
private int maxOverThresholdCount = 500;
//PhysicalMemory
private float deviceMemoryThreshold = 0.05f;
//FastHugeMemory
private float forceDumpJavaHeapMaxThreshold = 0.90f;
private final String REASON_HIGH_WATERMARK = "high_watermark";
private final String REASON_HUGE_DELTA = "delta";
private String mDumpReason = "";
private int forceDumpJavaHeapDeltaThreshold = 350_000;
public KOOMMoniter(Application app, ILog logImpl) {
application =app;
this.logImpl = logImpl;
handlerThread = new HandlerThread("KOOMMoniter");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
@Override
public void start() {
handler.postDelayed(this, CHECK_INTERVAL);
}
@Override
public void stop() {
handler.removeCallbacks(this);
}
@Override
public void report() {
}
@Override
public void run() {
SystemInfo.refresh();
if (isHeapOOMTracker() || isThreadOOMTracker() || isFDOOMTracjer() ||
isPhysicalMemoryOOMTracker() || isFastHugeMemoryOOMTracker()) {
dump();
}
handler.postDelayed(this, CHECK_INTERVAL);
}
private boolean isFastHugeMemoryOOMTracker() {
SystemInfo.JavaHeap javaHeap = SystemInfo.javaHeap;
// 高危閾值直接觸發(fā)dump分析
if (javaHeap.rate > forceDumpJavaHeapMaxThreshold) {
mDumpReason = REASON_HIGH_WATERMARK;
logImpl.Logi(TAG, "[meet condition] fast huge memory allocated detected, " +
"high memory watermark, force dump analysis!");
return true;
}
// 高差值直接dump
SystemInfo.JavaHeap lastJavaHeap = SystemInfo.lastJavaHeap;
if (lastJavaHeap.max != 0L && javaHeap.used - lastJavaHeap.used
> forceDumpJavaHeapDeltaThreshold * 1024.0f) {
mDumpReason = REASON_HUGE_DELTA;
logImpl.Logi(TAG, "[meet condition] fast huge memory allocated detected, " +
"over the delta threshold!");
return true;
}
return false;
}
private boolean isPhysicalMemoryOOMTracker() {
SystemInfo.MemInfo info = SystemInfo.memInfo;
if (info.rate < deviceMemoryThreshold) {
Log.i(TAG, "oom meminfo.rate < " + (deviceMemoryThreshold * 100) + "%");
//return true //先只是上傳,不真實觸發(fā)dump
} else if (info.rate < 0.10f) {
Log.i(TAG, "oom meminfo.rate < 10.0%");
} else if (info.rate < 0.15f) {
Log.i(TAG, "oom meminfo.rate < 15.0%");
} else if (info.rate < 0.20f) {
Log.i(TAG, "oom meminfo.rate < 20.0%");
} else if (info.rate < 0.30f) {
Log.i(TAG, "oom meminfo.rate < 30.0%");
}
return false;
}
private boolean isFDOOMTracjer() {
try {
File processFile = new File("/proc/self/fd");
int fdCount = processFile.listFiles().length;
if (fdCount > fdThreshold && fdCount >= mLastFdCount - FD_COUNT_THRESHOLD_GAP) {
mOverThresholdCount++;
logImpl.Logi(TAG,
"[meet condition] "
+ "overThresholdCount: $mOverThresholdCount"
+ ", fdCount: $fdCount");
// dumpFdIfNeed() 拿進程全部fd /proc/self/fd
} else {
resetFDTracjer();
}
mLastFdCount = fdCount;
} catch (Exception e) {
e.printStackTrace();
}
return mOverThresholdCount >= maxOverThresholdCount;
}
private void resetFDTracjer() {
mLastFdCount = 0;
mOverThresholdCount = 0;
}
private boolean isThreadOOMTracker() {
int threadCount = SystemInfo.procStatus.thread;
if (threadCount > threadThreshold
&& threadCount >= mLastThreadCount - THREAD_COUNT_THRESHOLD_GAP) {
mOverThresholdCount++;
logImpl.Logi(TAG,
"[meet condition] "
+ "overThresholdCount:$mOverThresholdCount"
+ ", threadCount: $threadCount");
//dumpThreadIfNeed() 拿線程id /proc/self/task
} else {
resetThreadTracker();
}
mLastThreadCount = threadCount;
return mOverThresholdCount >= threadMaxOverThresholdCount;
}
private void resetThreadTracker() {
mLastThreadCount = 0;
mOverThresholdCount = 0;
}
private boolean isHeapOOMTracker() {
float heapRatio = SystemInfo.javaHeap.rate;
if (heapRatio > heapThreshold && heapRatio >= mLastHeapRatio - HEAP_RATIO_THRESHOLD_GAP) {
mOverThresholdCount++;
logImpl.Logi(TAG,
"[meet condition] "
+ "overThresholdCount: $mOverThresholdCount"
+ ", heapRatio: $heapRatio"
+ ", usedMem: ${SizeUnit.BYTE.toMB(SystemInfo.javaHeap.used)}mb"
+ ", max: ${SizeUnit.BYTE.toMB(SystemInfo.javaHeap.max)}mb");
} else {
resetHeap();
}
mLastHeapRatio = heapRatio;
return mOverThresholdCount >= heapMaxOverThresholdCount;
}
private void resetHeap() {
mLastHeapRatio = 0.0f;
mOverThresholdCount = 0;
}
private void dump() {
Calendar calendar = Calendar.getInstance();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_hh-mm-ss");
String path = dateFormat.format(calendar.getTime()) + ".hprof";
File heapDumpFile = new File(application.getFilesDir(), path);
ForkJvmHeapDumper.getInstance().dumpAndAnalysisc(heapDumpFile.getAbsolutePath());
}
}
public class SystemInfo {
private static final String TAG = "SystemInfo";
static ProcStatus procStatus = new ProcStatus();
static ProcStatus lastProcStatus = new ProcStatus();
static MemInfo memInfo = new MemInfo();
static MemInfo lastMemInfo = new MemInfo();
static JavaHeap javaHeap = new JavaHeap();
static JavaHeap lastJavaHeap = new JavaHeap();
//selinux權(quán)限問題,先注釋掉
//var dmaZoneInfo: ZoneInfo = ZoneInfo()
//var normalZoneInfo: ZoneInfo = ZoneInfo()
public static void refresh() {
lastJavaHeap = javaHeap;
lastMemInfo = memInfo;
lastProcStatus = procStatus;
javaHeap = new JavaHeap();
procStatus = new ProcStatus();
memInfo = new MemInfo();
javaHeap.max = Runtime.getRuntime().maxMemory();
javaHeap.total = Runtime.getRuntime().totalMemory();
javaHeap.free = Runtime.getRuntime().freeMemory();
javaHeap.used = javaHeap.total - javaHeap.free;
javaHeap.rate = 1.0f * javaHeap.used / javaHeap.max;
try {
File processFile = new File("/proc/self/status");
BufferedReader br = new BufferedReader(new FileReader(processFile));
String line;
while ((line = br.readLine()) != null) {
if (procStatus.vssInKb != 0 && procStatus.rssInKb != 0 && procStatus.thread != 0) {
break;
}
if (line.startsWith("VmSize")) {
procStatus.vssInKb = Integer.parseInt(
line.replace(" ", "").replace("VmSize:", "").replace("kB", "").trim()
);
} else if (line.startsWith("VmRSS")) {
procStatus.rssInKb = Integer.parseInt(
line.replace(" ", "").replace("VmRSS:", "").replace("kB", "").trim()
);
} else if (line.startsWith("Threads")) {
procStatus.thread = Integer.parseInt(
line.replace(" ", "").replace("Threads:", "").trim()
);
}
}
} catch (Exception e) {
e.printStackTrace();
}
try {
File memFile = new File("/proc/meminfo");
BufferedReader br = new BufferedReader(new FileReader(memFile));
String line;
while ((line = br.readLine()) != null) {
if (procStatus.vssInKb != 0 && procStatus.rssInKb != 0 && procStatus.thread != 0) {
break;
}
if (line.startsWith("MemTotal")) {
memInfo.totalInKb = Integer.parseInt(
line.replace(" ", "").replace("MemTotal:", "").replace("kB", "").trim()
);
} else if (line.startsWith("MemFree")) {
memInfo.freeInKb = Integer.parseInt(
line.replace(" ", "").replace("MemFree:", "").replace("kB", "").trim()
);
} else if (line.startsWith("MemAvailable")) {
memInfo.availableInKb = Integer.parseInt(
line.replace(" ", "").replace("MemAvailable:", "").replace("kB", "").trim()
);
} else if (line.startsWith("CmaTotal")) {
memInfo.cmaTotal = Integer.parseInt(
line.replace(" ", "").replace("CmaTotal:", "").replace("kB", "").trim()
);
} else if (line.startsWith("ION_heap")) {
memInfo.IONHeap = Integer.parseInt(
line.replace(" ", "").replace("ION_heap:", "").replace("kB", "").trim()
);
}
}
} catch (Exception e) {
e.printStackTrace();
}
memInfo.rate = 1.0f * memInfo.availableInKb / memInfo.totalInKb;
Log.i(TAG, "----OOM Monitor Memory----");
Log.i(TAG, "[java] max:" + javaHeap.max + " used ratio:" + (javaHeap.rate * 100) + "%");
Log.i(TAG, "[proc] VmSize:" + procStatus.vssInKb + "kB VmRss:" + procStatus.rssInKb + "kB " + "Threads:" + procStatus.thread);
Log.i(TAG, "[meminfo] MemTotal:" + memInfo.totalInKb + "kB MemFree:" + memInfo.freeInKb + "kB " + "MemAvailable:" + memInfo.availableInKb + "kB");
Log.i(TAG, "avaliable ratio:" + (memInfo.rate * 100) + "% CmaTotal:" + memInfo.cmaTotal + "kB ION_heap:" + memInfo.IONHeap + "kB");
}
static class ProcStatus {
int thread;
int vssInKb;
int rssInKb;
}
static class MemInfo {
int totalInKb;
int freeInKb;
int availableInKb;
int IONHeap;
int cmaTotal;
float rate;
}
static class JavaHeap {
long max;
long total;
long free;
long used;
float rate;
}
}
dump的方式
1.開線程dump
優(yōu)點:簡單
缺點:在 dump 過程中會凍住進程,使其無法工作。
new Thread(new Runnable() {
@Override
public void run() {
Debug.dumpHprofData(path);
}
}).start();
2.開子進程dump
利用 linux 的 Copy-On-Write 機制,fork 的進程與父進程有相同的內(nèi)存,并且 fork 的時間也很短,在子進程中執(zhí)行 dump 過程中不會影響到父進程的運行。優(yōu)缺點與上面相反。
偽代碼如下
1.在 fork 前掛起所有的線程(native 層)
// art::Dbg::SuspendVM
void (*suspend_vm_fnc_)();
2.fork (native 層)
pid = fork();
if(pid == 0){
//在子進程中執(zhí)行相應(yīng)的操作
}else if(pid > 0){
//在父進程中
}else{
//fork出問題
}
3.fork 后恢復(fù) (native 層)
// art::Dbg::ResumeVM
void (*resume_vm_fnc_)();
4.子進程 dump (在 java 層),dump完成后退出進程(native 層)
Debug.dumpHprofData(path);
exit(0);
5.父進程等待子進程退出(native 層)
waitpid(pid, &status, 0) ;
6.父進程拿到 dump 后的 hprof 文件
這里的代碼涉及到查找 libart.so 中的掛起和恢復(fù),沒有足夠的能力寫,但大致的步驟是:
1.通過 dlopen() 打開加載 libart.so ,得到 so 的句柄 handle
2.通過 dlsym() 查找掛起和恢復(fù)函數(shù)的地址,(根據(jù)動態(tài)鏈接庫操作句柄與符號,返回符號對應(yīng)的地址)(每個 android 版本的 libart.so 中的掛起和恢復(fù)函數(shù)的符號會不一樣,這里需要兼容版本,readelf xxx.so 可查看函數(shù)符號)