关于如何定位这种问题其实可以把他拆分成两个问题,一个是调试阶段问题定位,另外一个就是线上问题定位,我们一步一步来分析
1.调试阶段问题定位 LeakCanary
为什么说LeakCanary 只能用于调试阶段的问题分析定位呢,那我还需要从 LeakCanary 的源码说起
整个Activity 检测过程
- 创建检测器任务队列
分发任务
public AndroidWatchExecutor(long initialDelayMillis) {
mainHandler = new Handler(Looper.getMainLooper());
HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
handlerThread.start();
backgroundHandler = new Handler(handlerThread.getLooper());
this.initialDelayMillis = initialDelayMillis;
maxBackoffFactor = Long.MAX_VALUE / initialDelayMillis;
}
注意上面代码 HandlerThread 的创建过程,以及他所导入的Looper , HandlerThread 会启动一个looper ,并且将这个looper 的 循环 开启,将这个looper 与这个Looper 所持有的MessageQueue 交给backgroundHandler 建立联系,那么以后我们使用这个backgroundHandler 调度的任何任务已经与MainThread 里面的looper 没有关系了, 这一点很重要,理解了这个地方那么下面的代码就能很好的理解了
@Override public void execute(Retryable retryable) {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
waitForIdle(retryable, 0);// 主线程直接使用idel
} else {
postWaitForIdle(retryable, 0); 子线程
}
}
private void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
mainHandler.post(new Runnable() {
@Override public void run() {
waitForIdle(retryable, failedAttempts);
}
});
}
private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
long delayMillis = initialDelayMillis * exponentialBackoffFactor;
backgroundHandler.postDelayed(new Runnable() {
@Override public void run() {
Retryable.Result result = retryable.run();
if (result == RETRY) {
postWaitForIdle(retryable, failedAttempts + 1);
}
}
}, delayMillis);
}
在分发事件的过程中如果在主线程,就使用主线程Looper 的idel 将这个线程转换到 子线程的 looper 中,如果是在子线程就将这个任务直接在子线程中的looper 中调度
一个监视任务被触发,他都干了什么呢,如何干的呢,下面继续分析
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
ensureGoneAsync(watchStartNanoTime, reference);
}
@SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
removeWeaklyReachableReferences();
if (debuggerControl.isDebuggerAttached()) {
// The debugger can create false leaks.
return RETRY;
}
if (gone(reference)) {
return DONE;
}
gcTrigger.runGc();
removeWeaklyReachableReferences();
if (!gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == RETRY_LATER) {
// Could not dump the heap.
return RETRY;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
.referenceName(reference.name)
.watchDurationMs(watchDurationMs)
.gcDurationMs(gcDurationMs)
.heapDumpDurationMs(heapDumpDurationMs)
.build();
heapdumpListener.analyze(heapDump);
}
return DONE;
}
我们先把代码放在上面,下面我们来分析他的代码都做了哪些事情
1.创建 KeyedWeakReference 弱引用 持有这个对象,因为弱引用不会影响对象的回收,并能在没有其他类型的引用关系时被gc回收
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
- 清空引用队列中已经回收的对象
removeWeaklyReachableReferences();
private void removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
KeyedWeakReference ref;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
retainedKeys.remove(ref.key);
}
}
我先来说一下引用队列的工作原理,当我们构建一个弱引用对象,并把他与引用队列做了关联,那么在这个被弱引用所引用的对象被回收时,这个对象的一些关联信息就会被存放到引用队列中
那么他这里的工作就是 遍历现有的队列中的已经被释放的对象,并把他的关系从我们的缓存的Set 中 删除,用来明确是否有其他地方导致的gc,说白了就是在能明确已经被回收的情况下不用在做gc 的操作了
- 再次确认引用缓存的set 中是否还有这个key的信息,如果被删除,那么set 中的key也会被删除,
if (gone(reference)) {
return DONE;
}
private boolean gone(KeyedWeakReference reference) {
return !retainedKeys.contains(reference.key);
}
- 手动触发gc
gcTrigger.runGc();
5.重复2的操作,查看改对象是否已经被回收
6.重复3的操作查看是否已经被回收 没有则发生内存泄露
7.读取内存快照
8.分析内存快照
为什么 LeakCanary 不能用于线上
频繁执行gc 导致程序卡顿更加剧烈
已经发出去版本频繁分析与解析内存快照的实际意义并不大,注意 这里说的是频繁的分析与解析, 因为内存快照可能很大,这个解析的过程也是一个比较复杂的过程,如果频繁发生内存泄露,就代表着需要频繁的上传文件, 很明显这里需要一个机制,来保证正确的内存收集已经,以及内存快照文件的上传,如果我们上传太多的这种文件,筛选和处理也是需要非常多的成本的
3.在 dump 内存快照信息的时候,jvm 会暂停所有操作, LeakCanary 的每次发生内存泄露的时候都会 去dump ,就导致卡顿
说道这里我们就需要知道哪些情况会导致OOM 的发生
- 内存泄露 无用的对象无法被回收,
- 内存抖动 频繁的创建和销毁对象会导致内存碎片化比较严重,内存总量是足够的 但是不连续,
- Thread过多 任务无法结束 或者同一时间节点创建的任务过多 也有可能是执行不频繁的线程池任务创建的主要线程数过多 ,导致主要线程池中的线程无法释放
- 内存不足
5.收集这些信息还用使用dump内存快照, 这时会 STOP THE WORLD ,那么如何来处理这个问题 ,
那么针对上面的四种情况我们能怎么拿到这种情况呢
内存抖动现阶段还没有一个比较成熟的方案,一种是koom 每15秒检测一下内存,发现如果内存急剧上升到危险值后获取dump来分析,通过快照文件发现哪个类的异常,另一种只能是在线下使用profiler 来查看这个类的内存表现是否存在明显的锯齿化的来判断,如果存在锯齿化这种情况在继续对他的对象来做分析,分析一个时间段哪个对象的创建和销毁比较频繁
内存泄露 内存泄露我们可以通过内存的快照文件获取他的引用信息
3.线程数 线程数可通过 读取 proc/self//status 这个文件来判断
- 内存不足 我们需要拿到用户手机分配给当前app 的实际情况来分析,需要查看他的 使用内存 所有内存 以及使用内存 来分析
5.使用子进程dump , 在主进程fork 子进程 , 在fork的开始时 ,linux 会将 主进程的内存区域的权限修改为 read only , 这样子进程和父进程都同时映射到了 同一块内存区域, 由于linux进程隔离,两个访问同一块内存是不合理的,如果 主进程或者子进程 任意一个修改这个内存,就会触发linux 的 copy on write ,将主进程的复制一份给子进程,所以在子进程中能dump 到主进程的信息
6 同时 由于dump 后的 hprof 这个文件比较大,存储的信息也比较多,如何筛选有用信息也是非常中用的一步
/// 各个数据的获取方式
这些分析的都是 java Heap 的一些内存分析,同时我们还需要对 进程已经
剩余内存 Runtime.getRuntime().freeMemory()
总分配内存 Runtime.getRuntime().totalMemory()
使用内存= 总分配内存 - 剩余内存
https://blog.csdn.net/whbing1471/article/details/105468139/ 我看了一下这博客说的很详细 关于 proc/self/state proc/meminfo proc 是 Process 简称 meminfo 是memery Info 的简称 ,都是linux 命令
我们具体查看的方式
就可以拿到系统给我们分配的内存信息以及线程信息
fun test(){
File("/proc/self/status").forEachLineQuietly { line ->
when {
line.startsWith("VmSize") -> {
Log.i("tian.shm","VmSize${VSS_REGEX.matchValue(line)}")
}
line.startsWith("VmRSS") -> {
Log.i("tian.shm","rssInKb${RSS_REGEX.matchValue(line)}")
}
line.startsWith("Threads") -> {
Log.i("tian.shm","thread${THREADS_REGEX.matchValue(line)}")
}
else ->{
}
}
}
File("/proc/meminfo").forEachLineQuietly { line ->
when {
line.startsWith("MemTotal") -> {
Log.i("tian.shm","totalInKb${MEM_TOTAL_REGEX.matchValue(line)}")
}
line.startsWith("MemFree") -> {
Log.i("tian.shm","freeInKb${MEM_FREE_REGEX.matchValue(line)}")
}
line.startsWith("MemAvailable") -> {
Log.i("tian.shm","availableInKb${MEM_AVA_REGEX.matchValue(line)}")
}
line.startsWith("CmaTotal") -> {
Log.i("tian.shm","cmaTotal${MEM_CMA_REGEX.matchValue(line)}")
}
line.startsWith("ION_heap") -> {
Log.i("tian.shm","IONHeap${MEM_ION_REGEX.matchValue(line)}")
}
else ->{
}
}
}
}
private fun Regex.matchValue(s: String) = matchEntire(s.trim())
?.groupValues?.getOrNull(1)?.toInt() ?: 0
private fun File.forEachLineQuietly(charset: Charset = Charsets.UTF_8, action: (line: String) -> Unit) {
kotlin.runCatching {
// Note: close is called at forEachLineQuietly
BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(action)
}.onFailure { exception -> exception.printStackTrace() }
}
线上问题检测 KOOM
快手研发的一个获取线上性能的库,给我们制定了一个获取当前运行状态的这么一个机制,这个保证了同一个版本 15天内 超过5次不会重复上传hprof 文件,以及运行时合适收集内存信息的一个机制,获取的方式就是使用我在上面所说的这样的方法,如果你需要从0-1建立这样一个缓存机制那么可以看一下他们的源码,如果你只是想了解他们使用什么方式获取的各个性能间的信息,那么上面的案例可以帮到你