Android平台内存分析

1、介绍
有的时候我们会发现应用堆内存和设备内存都非常充足,还是会出现内存分配失败的情况,这跟资源泄漏可能有比较大的关系。
1、文件句柄 fd。文件句柄的限制可以通过 /proc/self/limits 获得,一般单个进程允许打开的最大文件句柄个数为 1024。但是如果文件句柄超过 800 个就比较危险,需要将所有的 fd 以及对应的文件名输出到日志中,进一步排查是否出现了有文件或者线程的泄漏。
2、 线程数。当前线程数大小可以通过上面的 status 文件得到,一个线程可能就占 2MB 的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。根据我的经验来说,如果线程数超过 400 个就比较危险。需要将所有的线程 id 以及对应的线程名输出到日志中,进一步排查是否出现了线程相关的问题。
3、 JNI。使用 JNI 时,如果不注意很容易出现引用失效、引用爆表等一些崩溃。我们可以通过 DumpReferenceTables 统计 JNI 的引用表,进一步分析是否出现了 JNI 泄漏等问题。
在前面所讲的崩溃分析中,我提到过“内存优化”是崩溃优化工作中非常重要的一部分。类似 OOM,很多的“异常退出”其实都是由内存问题引起。
内存造成的第一个问题是异常 。在前面的崩溃分析我提到过“异常率”,异常包括 OOM、内存分配失败这些崩溃,也包括因为整体内存不足导致应用被杀死、设备重启等问题。不知道你平时是否在工作中注意过,如果我们把用户设备的内存分成 2GB 以下和 2GB 以上两部分,你可以试试分别计算他们的异常率或者崩溃率,看看差距会有多大。
内存造成的第二个问题是卡顿 。Java 内存不足会导致频繁 GC,这个问题在 Dalvik 虚拟机会更加明显。而 ART 虚拟机在内存管理跟回收策略上都做大量优化,内存分配和 GC 效率相比提升了 5~10 倍。如果想具体测试 GC 的性能,例如暂停挂起时间、总耗时、GC 吞吐量,我们可以通过发送 SIGQUIT 信号获得 ANR 日志。
adb shell kill -S QUIT PID
adb pull /data/anr/traces.txt
 
2、Android Bitmap 内存分配的变化
在 Android 3.0 之前,Bitmap 对象放在 Java 堆,而像素数据是放在 Native 内存中。如果不手动调用 recycle,Bitmap Native 内存的回收完全依赖 finalize 函数回调,熟悉 Java 的同学应该知道,这个时机不太可控。
Android 3.0~Android 7.0 将 Bitmap 对象和像素数据统一放到 Java 堆中,这样就算我们不调用 recycle,Bitmap 内存也会随着对象一起被回收。不过 Bitmap 是内存消耗的大户,把它的内存放到 Java 堆中似乎不是那么美妙。即使是最新的华为 Mate 20,最大的 Java 堆限制也才到 512MB,可能我的物理内存还有 5GB,但是应用还是会因为 Java 堆内存不足导致 OOM。Bitmap 放到 Java 堆的另外一个问题会引起大量的 GC,对系统内存也没有完全利用起来。
有没有一种实现,可以将 Bitmap 内存放到 Native 中,也可以做到和对象一起快速释放,同时 GC 的时候也能考虑这些内存防止被滥用?NativeAllocationRegistry 可以一次满足你这三个要求,Android 8.0 正是使用这个辅助回收 Native 内存的机制,来实现像素数据放到 Native 内存中。Android 8.0 还新增了硬件位图 Hardware Bitmap,它可以减少图片内存并提升绘制效率。
安装包中的代码、图片、资源以及 so 库的大小跟内存究竟有哪些关系?你可以参考下面的这个表格。
 
3、优化图片内存:
1、 大图片监控。我们需要注意某张图片内存占用是否过大,例如长宽远远大于 View 甚至是屏幕的长宽。在开发过程中,如果检测到不合规的图片使用,应该立即弹出对话框提示图片所在的 Activity 和堆栈,让开发同学更快发现并解决问题。在灰度和线上环境下可以将异常信息上报到后台,我们可以计算有多少比例的图片会超过屏幕的大小,也就是图片的“超宽率。
2、 重复图片监控。重复图片指的是 Bitmap 的像素数据完全一致,但是有多个不同的对象存在。这个监控不需要太多的样本量,一般只在内部使用。之前我实现过一个内存 Hprof 的分析工具,它可以自动将重复 Bitmap 的图片和引用链输出。下图是一个简单的例子,你可以看到两张图片的内容完全一样,通过解决这张重复图片可以节省 1MB 内存。
3、 图片总内存。通过收拢图片使用,我们还可以统计应用所有图片占用的内存,这样在线上就可以按不同的系统、屏幕分辨率等维度去分析图片内存的占用情况。在 OOM 崩溃的时候,也可以把图片占用的总内存、Top N 图片的内存都写到崩溃日志中,帮助我们排查问题。
 
4、Allocation Tracker 的三个缺点:
1、 获取的信息过于分散,中间夹杂着不少其他的信息,很多信息不是应用申请的,可能需要进行不少查找才能定位到具体的问题。
2、 跟 Traceview 一样,无法做到自动化分析,每次都需要开发者手工开始 / 结束,这对于某些问题的分析可能会造成不便,而且对于批量分析来说也比较困难。
3、 虽然在 Allocation Tracking 的时候,不会对手机本身的运行造成过多的性能影响,但是在停止的时候,直到把数据 dump 出来之前,经常会把手机完全卡死,如果时间过长甚至会直接 ANR。
 
5、Memory Churn内存抖动:
内存抖动是因为在短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象会严重占用内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。 这个操作有可能会影响到帧率,并使得用户感知到性能问题。
我们就可以快速知道发生内存抖动时,是因为哪些变量的创建造成频繁GC。一般来说我们需要注意以下几个方面:
1、 读文件优化 读文件使用ByteArrayPool,初始设置capacity,减少expand。
2、资源重用 建立全球缓存池,对频繁申请、释放的对象类型重用。
3、 减少不必要或不合理的对象 例如在ondraw、getview中应减少对象申请,尽量重用。更多是一些逻辑上的东西,例如循环中不断申请局部变量等
4、 选用合理的数据格式 使用SparseArray, SparseBooleanArray, and LongSparseArray来代替Hashmap。
Native 内存分析:
我们有没有类似 Allocation Tracker 那样的 Native 内存分配工具呢?在这方面,Android 目前的支持还不是太好,但 Android Developer 近来也补充了一些相关的文档,关于 Native 内存的问题,有两种方法,分别是 Malloc 调试和 Malloc 钩子。
 
6、LeakCanary内存侦测源码分析:
LeakCanary是一个内存泄漏检测的框架,默认只会检测Activity的泄漏,如果需要检测其他类,可以使用LeakCanary.install返回的RefWatcher,调用RefWatcher.watch(obj)就可以观测obj对象是否出现泄漏。
LeakCanary 检测只要在leakcanary-android包下,入口函数为ActivityRefWatcher。
 
 
Reference 把内存分为 4 种状态,Active 、 Pending 、 Enqueued 、 Inactive。
Active 一般说来内存一开始被分配的状态都是 Active。
Pending 快要放入队列(ReferenceQueue)的对象,也就是马上要回收的对象。
Enqueued 对象已经进入队列,已经被回收的对象。方便我们查询某个对象是否被回收。
Inactive 最终的状态,无法变成其他的状态。
public final class ActivityRefWatcher {
  public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }
 
  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityDestroyed(Activity activity) {
          //在activity的Destroyed时,监控activity是否被回收
          refWatcher.watch(activity);
        }
      };
}
 
观察
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);
  }
 
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }
 
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;
    }
    //引发GC
    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;
  }
 
内存泄漏分析首先采用Android自带的Debug来存储将当时的heap信息的导出文件,具体的方法为  Debug.dump H profData(heapDumpFile.get A bsolutePath());
LeakCanary 采用的是使用自家的分析库  com.squareup.haha  找到对象的引用树。
HeapAnalyzer
public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
    @NonNull String referenceKey,
    boolean computeRetainedSize) {
. 
  ...
  try {
   // 使用haha库分析堆栈
    listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
    HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
    HprofParser parser = new HprofParser(buffer);
    listener.onProgressUpdate(PARSING_HEAP_DUMP);
    Snapshot snapshot = parser.parse();
    listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
    // 把Snapshot里堆栈信息转存到THashMap,通过信息构建一个key
    deduplicateGcRoots(snapshot);
    listener.onProgressUpdate(FINDING_LEAKING_REF);
    // 找到泄漏对象
    Instance leakingRef = findLeakingReference(referenceKey, snapshot);
 
 
    // False alarm, weak reference was cleared in between key check and heap dump.
    if (leakingRef == null) {
      String className = leakingRef.getClassObj().getClassName();
      return noLeak(className, since(analysisStartNanoTime));
    }
    return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
  } catch (Throwable e) {
    return failure(e, since(analysisStartNanoTime));
  }
}
 
void deduplicateGcRoots(Snapshot snapshot) {
        // THashMap has a smaller memory footprint than HashMap.
        // THashMap的内存占用量比HashMap小。
        final THashMap<String, RootObj> uniqueRootMap = new THashMap<>();
 
 
        final Collection<RootObj> gcRoots = snapshot.getGCRoots();
        for (RootObj root : gcRoots) {
            String key = generateRootKey(root);
            if (!uniqueRootMap.containsKey(key)) {
                uniqueRootMap.put(key, root);
            }
        }
 
 
        // Repopulate snapshot with unique GC roots.
        gcRoots.clear();
        uniqueRootMap.forEach(new TObjectProcedure<String>() {
            @Override
            public boolean execute(String key) {
                return gcRoots.add(uniqueRootMap.get(key));
            }
        });
    }
 
private Instance findLeakingReference(String key, Snapshot snapshot) {
  ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
  if (refClass == null) {
    throw new IllegalStateException(
        "Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
  }
  List<String> keysFound = new ArrayList<>();
  for (Instance instance : refClass.getInstancesList()) {
    List<ClassInstance.FieldValue> values = classInstanceValues(instance);
    Object keyFieldValue = fieldValue(values, "key");
    if (keyFieldValue == null) {
      keysFound.add(null);
      continue;
    }
    String keyCandidate = asString(keyFieldValue);
    if (keyCandidate.equals(key)) {
      return fieldValue(values, "referent");
    }
    keysFound.add(keyCandidate);
  }
  throw new IllegalStateException(
      "Could not find weak reference with key " + key + " in " + keysFound);
}
 
//核心代码
private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
    Instance leakingRef, boolean computeRetainedSize) {
 
 
  listener.onProgressUpdate(FINDING_SHORTEST_PATH);
  ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
  //找到寻找最短引用路径
  ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
 
 
  String className = leakingRef.getClassObj().getClassName();
 
 
  // False alarm, no strong reference path to GC Roots.
  if (result.leakingNode == null) {
    return noLeak(className, since(analysisStartNanoTime));
  }
 
 
  listener.onProgressUpdate(BUILDING_LEAK_TRACE);
  LeakTrace leakTrace = buildLeakTrace(result.leakingNode);
 
 
  long retainedSize;
  if (computeRetainedSize) {
 
 
    listener.onProgressUpdate(COMPUTING_DOMINATORS);
    // Side effect: computes retained size.
    snapshot.computeDominators();
 
 
    Instance leakingInstance = result.leakingNode.instance;
 
 
    retainedSize = leakingInstance.getTotalRetainedSize();
 
 
    // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
    if (SDK_INT <= N_MR1) {
      listener.onProgressUpdate(COMPUTING_BITMAP_SIZE);
      retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
    }
  } else {
    retainedSize = AnalysisResult.RETAINED_HEAP_SKIPPED;
  }
 
ShortestPathFinder
  1. 整个内存信息抽象为一个以GCRoot为根的树
  2. 采用从根节点出发,采用类似广度优先的搜索策略
  3. 每遍历一个类,new一个LeakNode对象,LeakNode保存了父LeakNode的引用
  4. 遍历到泄露的对象是,返回本对象LeakNode的即可。
  5. 从本LeakNode出发,先前便利,就可以找到最短引用路径。
 
 
 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android ION(IPC over NETwork)是一种使用Android的Binder IPC通信机制进行进程间通信的开源库。在进行内存分析时,我们可以通过以下几个步骤来分析Android ION的内存使用情况。 首先,我们可以使用内存分析工具,如Android Studio提供的Memory Profiler,通过单击“Start Recording”按钮开始记录内存使用情况。然后,我们可以执行包含Android ION代码的操作,例如分配和释放ION内存缓冲区。 接下来,我们可以使用Memory Profiler工具来分析我们的应用程序在执行这些操作时所使用的内存。工具会提供一个内存使用情况的图表,我们可以根据这个图表来判断再分配和释放ION内存缓冲区时是否存在内存泄漏或者过度内存使用的情况。 此外,我们还可以使用Android的Debug.MemoryInfo API来获取有关Android ION内存使用情况的更详细信息。通过使用MemoryInfo对象的getMemoryStats()方法,我们可以获取关于Android ION的内存使用情况的一些统计数据,例如分配的内存缓冲区的数量、总大小等。 最后,在进行完内存分析后,我们可以根据分析结果来优化我们的应用程序。例如,我们可以检查我们的代码是否正确地释放了ION内存缓冲区,并确保我们只分配了我们实际需要的内存大小。我们还可以考虑使用更有效的算法来减少对ION内存缓冲区的需求,从而降低内存使用量。 总结来说,通过使用内存分析工具和Android的Debug.MemoryInfo API,我们可以对Android ION的内存使用情况进行分析和优化,以确保我们的应用程序能够有效地使用ION内存缓冲区,并避免内存泄漏和过度内存使用的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值