9.優(yōu)化 - 內(nèi)存優(yōu)化之dump時機及方式

上一次講到內(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ù)符號)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容