koom koom-java-leak 源码分析

本文详细分析了Koom库中针对Java内存泄漏的检测和处理机制,包括AnalysisExtraData的分析信息,HeapReport的上报,OOMReportUploader的问题上传,以及不同类型的堆dump方法。同时介绍了内存和文件管理,系统信息获取,以及OOMMonitor如何根据多种跟踪器判断并触发内存dump分析。
摘要由CSDN通过智能技术生成

1.AnalysisExtraData.kt 包含原因,分析时间,当前页面信息

class AnalysisExtraData {
  var reason: String? = null        //reason原因

  var usageSeconds: String? = null  //时间

  var currentPage: String? = null   //当前页面
}

2.HeapReport上报的信息

public class HeapReport {

    public RunningInfo runningInfo = new RunningInfo();
    public List<GCPath> gcPaths = new ArrayList<>();//引用链gc path of suspected objects
    public List<ClassInfo> classInfos = new ArrayList<>();//类及其实例数量Class's instances count list
    public List<LeakObject> leakObjects = new ArrayList<>();//泄漏的对象
    public Boolean analysisDone;//flag to record whether hprof is analyzed already.
    public Integer reAnalysisTimes;//flag to record hprof reanalysis times.

    //device and app running info
    public static class RunningInfo {
        //JVM info
        public String jvmMax;//jvm max memory in MB jvm最大内存
        public String jvmUsed;//jvm used memory in MB jvm已经用过多少了

        //https://my.oschina.net/u/4592355/blog/5004330
        //https://www.cnblogs.com/liyuanhong/articles/7839762.html
        //在linux下表示内存的耗用情况有四种不同的表现形式:
        // VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
        // RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)
        // PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
        // USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
        //memory info
        public String vss;//vss memory in MB 进程vss
        public String pss;//pss memory in MB 进程pss
        public String rss;//rss memory in MB 进程rss
        public String threadCount;//线程个数
        public String fdCount;    //fd个数
        public List<String> threadList = new ArrayList<>();//线程信息
        public List<String> fdList = new ArrayList<>();    //fd信息

        //Device info
        public String sdkInt;       //sdk版本
        public String manufacture;  //厂商
        public String buildModel;   //版本

        //App info
        public String appVersion;
        public String currentPage;  //页面
        public String usageSeconds; //耗费时间
        public String nowTime;      //上报时间
        public String deviceMemTotal;     //device内存
        public String deviceMemAvaliable; //device可用内存

        public String dumpReason;//heap dump trigger reason,dump原因
        public String analysisReason;//analysis trigger reason,分析原因

        //KOOM Perf data
        public String koomVersion;
        public String filterInstanceTime; //过滤泄漏对象所花的时间
        public String findGCPathTime;     //发现GC引用链时间
    }

    /**
     * GC Path means path of object to GC Root, it can also be called as reference chain.
     */
    public static class GCPath {
        public Integer instanceCount;//todo instances number of same path to gc root
        public String leakReason;//reason of why instance is suspected
        public String gcRoot;
        public String signature;//signature are computed by the sha1 of reference chain
        public List<PathItem> path = new ArrayList<>();

        //引用链Item
        public static class PathItem {
            String reference;//referenced instance's classname + filed name
            String referenceType;//todo such as INSTANCE_FIELD/ARRAY_ENTRY/STATIC_FIELD
            String declaredClass;//对于从祖先类继承字段的情况。for cases when filed is inherited from ancestor's class.
        }
    }

    /**
     * ClassInfo contains data which describes the instances number of the Class.
     * ClassInfo 包含描述类的实例数量的数据。
     */
    public static class ClassInfo {
        public String className;
        public String instanceCount;//类的实例数量All instances's count of this class.
        public String leakInstanceCount;//All leaked instances's count of this class.
    }

    public static class LeakObject {
        public String className;
        public String size;
        public String objectId;
        public String extDetail;//todo
    }
}

3.OOMReportUploader.kt 上传问题

interface OOMReportUploader {
  /**
   * 注意:外部调用完upload后,切记自行删除
   */
  fun upload(file: File, content: String)
}

4.OOMHprofUploader 上传hprof

interface OOMHprofUploader {

  enum class HprofType {
    ORIGIN, STRIPPED
  }

  /**
   * 注意:外部调用完upload后,切记自行删除
   */
  fun upload(file: File, type: HprofType)
}

5.OOMPreferenceManager 保存分析次数,第一次启动时间

internal object OOMPreferenceManager {
    private const val PREFERENCE_NAME = "koom_hprof_analysis"

    private val mPreferences by lazy { mSharedPreferencesInvoker(PREFERENCE_NAME) }

    private lateinit var mSharedPreferencesInvoker: (String) -> SharedPreferences
    private lateinit var mPrefix: String

    fun init(sharedPreferencesInvoker: (String) -> SharedPreferences) {
        mSharedPreferencesInvoker = sharedPreferencesInvoker
        mPrefix = "${MonitorBuildConfig.VERSION_NAME}_"
    }

  //分析次数
    fun getAnalysisTimes(): Int {
        return mPreferences.getInt("${mPrefix}times", 0)
    }
  
    fun increaseAnalysisTimes() {
        mPreferences.edit()
                .also { clearUnusedPreference(mPreferences, it) }
                .putInt("${mPrefix}times", mPreferences.getInt("${mPrefix}times", 0) + 1)
                .apply()
    }

  //第一次启动时间
    fun getFirstLaunchTime(): Long {
        var time = mPreferences.getLong("${mPrefix}first_analysis_time", 0)
        if (time == 0L) {
            time = System.currentTimeMillis()
            setFirstLaunchTime(time)
        }
        return time
    }

    fun setFirstLaunchTime(time: Long) {
        if (mPreferences.contains("${mPrefix}first_analysis_time")) {
            return
        }

        return mPreferences.edit()
                .putLong("${mPrefix}first_analysis_time", time)
                .apply()
    }

  //清除之前版本的
    private fun clearUnusedPreference(//清除之前版本的
            preferences: SharedPreferences,
            editor: SharedPreferences.Editor
    ) {
        for (key in preferences.allKeys) {
            if (!key.startsWith(mPrefix)) {
                editor.remove(key)
            }
        }
    }
}

6.OOMFileManager 文件相关的

internal object OOMFileManager {
    private const val TIME_FORMAT = "yyyy-MM-dd_HH-mm-ss_SSS"

    private lateinit var mRootDirInvoker: (String) -> File//mRootDirInvoker表达式
    private lateinit var mPrefix: String    //前缀是个version

    private lateinit var mRootPath: String  //root路径

    val rootDir by lazy {
        if (this::mRootDirInvoker.isInitialized)//已初始化
            mRootDirInvoker("oom")
        else
            File(mRootPath)
    }

    @JvmStatic
    val hprofAnalysisDir by lazy { File(rootDir, "memory/hprof-aly").apply { mkdirs() } }

    @JvmStatic
    val manualDumpDir by lazy { File(rootDir, "memory/hprof-man").apply { mkdirs() } }

    @JvmStatic
    val threadDumpDir by lazy { File(hprofAnalysisDir, "thread").apply { mkdirs() } }

    @JvmStatic
    val fdDumpDir by lazy { File(hprofAnalysisDir, "fd").apply { mkdirs() } }

    @JvmStatic
    fun init(rootDirInvoker: (String) -> File) {//有两个init,如果有mRootDirInvoker,则以mRootDirInvoker为主
        mRootDirInvoker = rootDirInvoker
        mPrefix = "${MonitorBuildConfig.VERSION_NAME}_"
    }

    @JvmStatic
    fun init(rootPath: String?) {//有两个init,如果没有有mRootDirInvoker,以mRootPath为主
        if (rootPath != null) {
            mRootPath = rootPath
        }
        mPrefix = "${MonitorBuildConfig.VERSION_NAME}_"
    }

    @JvmStatic
    fun createHprofAnalysisFile(date: Date): File {
        val time = SimpleDateFormat(TIME_FORMAT, Locale.CHINESE).format(date)
        return File(hprofAnalysisDir, "$mPrefix$time.hprof").also {
            hprofAnalysisDir.mkdirs()
        }
    }

    @JvmStatic
    fun createJsonAnalysisFile(date: Date): File {
        val time = SimpleDateFormat(TIME_FORMAT, Locale.CHINESE).format(date)
        return File(hprofAnalysisDir, "$mPrefix$time.json").also {
            hprofAnalysisDir.mkdirs()
        }
    }

    @JvmStatic
    fun createHprofOOMDumpFile(date: Date): File {
        val time = SimpleDateFormat(TIME_FORMAT, Locale.CHINESE).format(date)
        return File(manualDumpDir, "$mPrefix$time.hprof").also {
            manualDumpDir.mkdirs()
        }
    }

    @JvmStatic
    fun createDumpFile(dumpDir: File): File {
        return File(dumpDir, "dump.txt").also {
            dumpDir.mkdirs()
        }
    }

    @JvmStatic
    fun isSpaceEnough(): Boolean {
        //https://blog.csdn.net/suyimin2010/article/details/86680731
        //getPath() 方法跟创建 File 对象时传入的路径参数有关,返回构造时传入的路径
        //getAbsolutePath() 方法返回文件的绝对路径,如果构造的时候是全路径就直接返回全路径,如果构造时是相对路径,就返回当前目录的路径 + 构造 File 对象时的路径
        //getCanonicalPath() 方法返回绝对路径,会把 ..\ 、.\ 这样的符号解析掉
        val statFs = StatFs(hprofAnalysisDir.canonicalPath)
        val blockSize = statFs.blockSizeLong//文件系统上块的大小(以字节为单位)
        val availableBlocks = statFs.availableBlocks.toLong()

        return blockSize * availableBlocks > 1.2 * 1024 * 1024//todo 1.2G ? 1.2M
    }
}

7.OOMHeapDumper dump有四种,simple + fork + strip + forkstrip

//dump有四种,simple+fork+strip+forkstrip
object OOMHeapDumper {
  private const val TAG = "OOMHeapDumper"

  private fun dump(dumper: HeapDumper) {
    try {
      MonitorLog.i(TAG, "dump hprof start")

      val hprofFile = OOMFileManager.createHprofOOMDumpFile(Date())

      val start = System.currentTimeMillis()

      hprofFile.createNewFile()
      dumper.dump(hprofFile.absolutePath)

      val end = System.currentTimeMillis()

      MonitorLog.i(TAG, "dump hprof complete," +
          " dumpTime:" + (end - start) +
          " fileName:" + hprofFile.name +
          " origin fileSize:" + SizeUnit.BYTE.toMB(hprofFile.length()) +
          " JVM max memory:" + SizeUnit.BYTE.toMB(Runtime.getRuntime().maxMemory()) +
          " JVM  free memory:" + SizeUnit.BYTE.toMB(Runtime.getRuntime().freeMemory()) +
          " JVM total memory:" + SizeUnit.BYTE.toMB(Runtime.getRuntime().totalMemory()), true)
    } catch (e: Throwable) {
      e.printStackTrace()

      MonitorLog.i(TAG, "dumpStripHprof failed: ${e.message}")
    }
  }

  @JvmStatic
  fun simpleDump() {
    MonitorLog.i(TAG, "simpleDump")
    dump(StandardHeapDumper())
  }

  @JvmStatic
  fun forkDump() {
    MonitorLog.i(TAG, "forkDump")
    dump(ForkJvmHeapDumper())
  }

  @JvmStatic
  fun stripDump() {
    MonitorLog.i(TAG, "dumpStripHprof")
    dump(StripHprofHeapDumper())
  }

  @JvmStatic
  fun forkDumpStrip() {
    MonitorLog.i(TAG, "forkDumpStrip")

    dump(ForkStripHeapDumper())
  }

}

8.OOMMonitorConfig配置

class OOMMonitorConfig(
        val analysisMaxTimesPerVersion: Int,  //每个版本最多分析次数
        val analysisPeriodPerVersion: Int,    //每个版本的前15天才分析,超过这个时间段不再dump

        val heapThreshold: Float,         //heap阈值
        val fdThreshold: Int,             //fd阈值
        val threadThreshold: Int,         //线程阈值
        val deviceMemoryThreshold: Float, //device memory阈值 还剩多少容量? todo
        val maxOverThresholdCount: Int,   //超过最大次数阈值 3次 todo
        val forceDumpJavaHeapMaxThreshold: Float,//强制dump java heap最大阈值 0.9大于等于heapThreshold todo
        val forceDumpJavaHeapDeltaThreshold: Int,//强制dump java heap delta阈值 Delta 三角洲 意思是增量 todo

        val loopInterval: Long, //轮询间隔

        val enableHprofDumpAnalysis: Boolean,//是否开启hprofdump分析

        val hprofUploader: OOMHprofUploader?,//hprof上传
        val reportUploader: OOMReportUploader?//日志上传
) : MonitorConfig<OOMMonitor>() {

    class Builder : MonitorConfig.Builder<OOMMonitorConfig> {
        companion object {
            private val DEFAULT_HEAP_THRESHOLD by lazy {
                val maxMem = SizeUnit.BYTE.toMB(Runtime.getRuntime().maxMemory())
                when {
                    maxMem >= 512 - 10 -> 0.8f
                    maxMem >= 256 - 10 -> 0.85f
                    else -> 0.9f
                }
            }

            private val DEFAULT_THREAD_THRESHOLD by lazy {
                if (MonitorBuildConfig.ROM == "EMUI" && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) {
                    450
                } else {
                    750
                }
            }
        }

        private var mAnalysisMaxTimesPerVersion = 5                         //每个版本最多分析5次
        private var mAnalysisPeriodPerVersion = 15 * 24 * 60 * 60 * 1000    //每个版本的前15天才分析,超过这个时间段不再dump

        private var mHeapThreshold: Float? = null
        private var mVssSizeThreshold = 3_650_000 //Only for 32 bit cpu devices. 360M todo 有用到吗
        private var mFdThreshold = 1000             //fd阈值1000
        private var mThreadThreshold: Int? = null   //线程阈值
        private var mDeviceMemoryThreshold: Float = 0.05f//todo
        private var mForceDumpJavaHeapMaxThreshold = 0.90f//达到javaheap的90%的时候强制dump todo
        private var mForceDumpJavaHeapDeltaThreshold = 350_000 //java heap rise 350m in a very short time.短时间增量350M的时候
        private var mMaxOverThresholdCount = 3//超过3次
        private var mLoopInterval = 15_000L //15s轮询一次

        private var mEnableHprofDumpAnalysis = true //enable hprof analysis

        private var mHprofUploader: OOMHprofUploader? = null
        private var mReportUploader: OOMReportUploader? = null

        fun setAnalysisMaxTimesPerVersion(analysisMaxTimesPerVersion: Int) = apply {
            mAnalysisMaxTimesPerVersion = analysisMaxTimesPerVersion
        }

        fun setAnalysisPeriodPerVersion(analysisPeriodPerVersion: Int) = apply {
            mAnalysisPeriodPerVersion = analysisPeriodPerVersion
        }

        /**
         * @param heapThreshold: 堆内存的使用比例[0.0, 1.0]
         */
        fun setHeapThreshold(heapThreshold: Float) = apply {
            mHeapThreshold = heapThreshold
        }

        /**
         * @param vssSizeThreshold: 单位是kb
         */
        fun setVssSizeThreshold(vssSizeThreshold: Int) = apply {
            mVssSizeThreshold = vssSizeThreshold
        }

        fun setFdThreshold(fdThreshold: Int) = apply {
            mFdThreshold = fdThreshold
        }

        fun setThreadThreshold(threadThreshold: Int) = apply {
            mThreadThreshold = threadThreshold
        }

        fun setMaxOverThresholdCount(maxOverThresholdCount: Int) = apply {
            mMaxOverThresholdCount = maxOverThresholdCount
        }

        fun setLoopInterval(loopInterval: Long) = apply {
            mLoopInterval = loopInterval
        }

        fun setEnableHprofDumpAnalysis(enableHprofDumpAnalysis: Boolean) = apply {
            mEnableHprofDumpAnalysis = enableHprofDumpAnalysis
        }

        fun setDeviceMemoryThreshold(deviceMemoryThreshold: Float) = apply {
            mDeviceMemoryThreshold = deviceMemoryThreshold
        }

        fun setForceDumpJavaHeapDeltaThreshold(forceDumpJavaHeapDeltaThreshold: Int) = apply {
            mForceDumpJavaHeapDeltaThreshold = forceDumpJavaHeapDeltaThreshold
        }

        fun setForceDumpJavaHeapMaxThreshold(forceDumpJavaHeapMaxThreshold: Float) = apply {
            mForceDumpJavaHeapMaxThreshold = forceDumpJavaHeapMaxThreshold
        }

        fun setHprofUploader(hprofUploader: OOMHprofUploader) = apply {
            mHprofUploader = hprofUploader
        }

        fun setReportUploader(reportUploader: OOMReportUploader) = apply {
            mReportUploader = reportUploader
        }

        override fun build() = OOMMonitorConfig(
                analysisMaxTimesPerVersion = mAnalysisMaxTimesPerVersion,
                analysisPeriodPerVersion = mAnalysisPeriodPerVersion,

                heapThreshold = mHeapThreshold ?: DEFAULT_HEAP_THRESHOLD,
                fdThreshold = mFdThreshold,
                threadThreshold = mThreadThreshold ?: DEFAULT_THREAD_THRESHOLD,
                deviceMemoryThreshold = mDeviceMemoryThreshold,
                maxOverThresholdCount = mMaxOverThresholdCount,
                loopInterval = mLoopInterval,
                enableHprofDumpAnalysis = mEnableHprofDumpAnalysis,

                forceDumpJavaHeapMaxThreshold = mForceDumpJavaHeapMaxThreshold,
                forceDumpJavaHeapDeltaThreshold = mForceDumpJavaHeapDeltaThreshold,

                hprofUploader = mHprofUploader,
              
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值