10.优化 - 内存优化之hprof文件处理

  在对 hprof 文件进行处理前,首先需要对 hprof 文件格式有所了解。Android dump 的 hprof 文件和 java 的有一点点不一样,它在 java hprof 文件的基础上增加了几项,但文件格式是一样。所以需要先介绍下 java 的 hprof 文件格式,再介绍怎么处理 hprof 文件

hprof 文件格式

参考自 java openjdk8 hprof 文件格式

  在 java 中,hprof 文件有2部分组成,一部分是 hprof head,一部分是 hprof body。其中 head 比较简单,由版本号,IDSize和时间组成。


HPROF-HEAD.png

  body 的类型比较多,但是有规律,是由一系列的 Record 组成,由1个字节的Tag、4个字节的Time、4个字节的Length和Body组成,Tag表示该Record的类型,Body部分为该Record的内容,长度为Length。

HPROF-Record.png

以读取 STRING IN UTF8 为例
STRING IN UTF8 的结构如图:

HPROF-STRING.png

                final int tag = mStreamIn.read();
                final int timestamp = IOUtil.readBEInt(mStreamIn);
                final long length = IOUtil.readBEInt(mStreamIn) & 0x00000000FFFFFFFFL;
                switch (tag) {
                    case HprofConstants.RECORD_TAG_STRING:
                        acceptStringRecord(timestamp, length, hv);
                        break;



    private void acceptStringRecord(int timestamp, long length, HprofVisitor hv) throws IOException {
        final ID id = IOUtil.readID(mStreamIn, mIdSize);
        final String text = IOUtil.readString(mStreamIn, length - mIdSize);
        hv.visitStringRecord(id, text, timestamp, length);
    }

    public static String readString(InputStream in, long length) throws IOException {
        final byte[] buf = new byte[(int) length];
        readFully(in, buf, 0, length);
        return new String(buf, Charset.forName("UTF-8"));
    }

    public static void readFully(InputStream in, byte[] buf, int off, long length) throws IOException {
        int n = 0;
        while (n < length) {
            final int count = in.read(buf, n, (int) (length - n));
            if (count < 0) {
                throw new EOFException();
            }
            n += count;
        }
    }

  先读取1个字节 Tag、4个字节的 Time 和4个字节的 Length,之后再根据读取到的 tag 匹配相关的 Record,这里就是 tag == 0x01 来匹配的,匹配到是 STRING 之后再读取 length - ID 长度的内容为 value。到此 string 的解析完成。其余的也是相同的解析思路。

  在 body 中有比较重要的是 HEAP DUMP 和 HEAP DUMP SEGMENT,这二块在 body 中占的比重比较大。而且 Android 在这里新增了android 特有的模块。

//传统模块
public static final int HEAPDUMP_ROOT_UNKNOWN = 0xff;

public static final int HEAPDUMP_ROOT_JNI_GLOBAL = 0x1;
public static final int HEAPDUMP_ROOT_JNI_LOCAL = 0x2;
public static final int HEAPDUMP_ROOT_JAVA_FRAME = 0x3;
public static final int HEAPDUMP_ROOT_NATIVE_STACK = 0x4;
public static final int HEAPDUMP_ROOT_STICKY_CLASS = 0x5;
public static final int HEAPDUMP_ROOT_THREAD_BLOCK = 0x6;
public static final int HEAPDUMP_ROOT_MONITOR_USED = 0x7;
public static final int HEAPDUMP_ROOT_THREAD_OBJECT = 0x8;
public static final int HEAPDUMP_ROOT_CLASS_DUMP = 0x20;
public static final int HEAPDUMP_ROOT_INSTANCE_DUMP = 0x21;
public static final int HEAPDUMP_ROOT_OBJECT_ARRAY_DUMP = 0x22;
public static final int HEAPDUMP_ROOT_PRIMITIVE_ARRAY_DUMP = 0x23;
//android 特有
public static final int HEAPDUMP_ROOT_HEAP_DUMP_INFO = 0xfe;
public static final int HEAPDUMP_ROOT_INTERNED_STRING = 0x89;
public static final int HEAPDUMP_ROOT_FINALIZING = 0x8a;
public static final int HEAPDUMP_ROOT_DEBUGGER = 0x8b;
public static final int HEAPDUMP_ROOT_REFERENCE_CLEANUP = 0x8c;
public static final int HEAPDUMP_ROOT_VM_INTERNAL = 0x8d;
public static final int HEAPDUMP_ROOT_JNI_MONITOR = 0x8e;
public static final int HEAPDUMP_ROOT_UNREACHABLE = 0x90;  /* deprecated */
public static final int HEAPDUMP_ROOT_PRIMITIVE_ARRAY_NODATA_DUMP = 0xc3;

在网上及开发者网站上没有找到详细介绍,只能根据代码大概的写出来android特有的模块(基于 matrix 源码 com.tencent.matrix.resource.hproflib.HprofConstants,在art/runtime/hprof/hprof.cc 也有)

name tag value
ROOT HEAP DUMP INFO 0xfe heapId-U4 , heapNameId-ID
ROOT INTERNED STRING 0x89 -ID
ROOT FINALIZING 0x8a -ID
ROOT DEBUGGER 0x8b -ID
ROOT REFERENCE_CLEANUP 0x8c -ID
ROOT VM INTERNAL 0x8d -ID
ROOT JNI MONITOR 0x8e id-ID , threadSerialNumber-U4 , stackDepth-U4
ROOT UNREACHABLE 0x90 -ID
ROOT PRIMITIVE ARRAY NODATA DUMP 0xc3 id-ID , stackId-U4 , numElements-U4 , typeId-U1 , elements-[numElements * ID] * U1

说明:value 中 name-size , name 是其名字(有可能没有),size 是其大小。

对于 body 中的 HEAP DUMP 和 HEAP DUMP SEGMENT 解析举个例子。


HPROF-ROOT_JNI_LOCAL.png
//1.先解析 record 的前三项,根据 tag == 0x0c || tag == 0x1c 来匹配 Record 的类型是 HEAP DUMP 或 HEAP DUMP SEGMENT
                final int tag = mStreamIn.read();
                final int timestamp = IOUtil.readBEInt(mStreamIn);
                final long length = IOUtil.readBEInt(mStreamIn) & 0x00000000FFFFFFFFL;
                switch (tag) {
                    case HprofConstants.RECORD_TAG_HEAP_DUMP:
                    case HprofConstants.RECORD_TAG_HEAP_DUMP_SEGMENT:
                        acceptHeapDumpRecord(tag, timestamp, length, hv);
                        break;
//2.再读出 1 个字节的子 tag == 0x02 来匹配 ROOT_JNI_LOCAL ,同时将上方的 length 减 1( length == 0 时表示 HEAP DUMP 或 HEAP DUMP SEGMENT 类型的 Record 解析完成)
            final int heapDumpTag = mStreamIn.read();
            --length;
            switch (heapDumpTag) {
                case HprofConstants.HEAPDUMP_ROOT_JNI_LOCAL:
                    length -= acceptJniLocal(hdv);
                    break;
//3.接着读出一个 IDSize 和 二个 U4 。表示 ROOT_JNI_LOCAL 子类型解析完毕
    private int acceptJniLocal(HprofHeapDumpVisitor hdv) throws IOException {
        final ID id = IOUtil.readID(mStreamIn, mIdSize);
        final int threadSerialNumber = IOUtil.readBEInt(mStreamIn);
        final int stackFrameNumber = IOUtil.readBEInt(mStreamIn);
        hdv.visitHeapDumpJniLocal(id, threadSerialNumber, stackFrameNumber);
        return mIdSize + 4 + 4;
    }

处理 hprof 文件

处理 hprof 分为二类,,一是裁剪 hprof 文件,保留分析 OOM 的数据,使得在手机上的文件更容易的上传到后台分析。二是直接在手机上对 hprof 文件进行分析,中有用的信息。

1.裁剪 hprof 文件

  hprof 文件中绝大部分数据是 PRIMITIVE ARRAY DUMP,通常占据80%以上,而分析 OOM 只关系对象的大小和引用关系,并不关心内容,因此这部分是裁剪的突破口。

  • Matrix
      Matrix 的 Hprof 文件裁剪功能的目标是将 Bitmap 和 String 以外的全部对象的基础类型数组的值移除,由于 Hprof 文件的分析功能只须要用到字符串数组和 Bitmap 的 buffer 数组。另外一方面,若是存在不一样的 Bitmap 对象其 buffer 数组值相同的状况,则能够将它们指向同一个 buffer,以进一步减少文件尺寸。
      主要流程为
      1.先寻找 bitmap、String、mBuffer、value 等类型的字符串索引。
      2.从字符串索引中找到相应的类索引。
      3.对于 bitmap 实例类会遍历其属性看是否有 mBuffer 与之匹配,如果有 2 次以上匹配的话就说明这个 bitmap 存在重复。对于 string 实例类会遍历其属性看是否有 value 与之匹配,匹配则记录到集合中。
      4.最后在 array 写入时判断其 ID 是否等于步骤3中重复的 bitmap ID ,等于则不写入。或者判断其 ID 是否等于步骤3中 string 记录的集合中的 ID,不在集合中则写入。

matrix 裁剪分析

  • KOOM
      KOOM 的裁剪方式和 Martix 不同,他是在 dump hprof 文件的过程中对文件的 open 和 write 进行了 plt hook,判断其是否需要裁剪写入,主要裁剪的也是 array 中的基本数据类型数组,避免了打开 hprof 文件时 OOM。

源码在 KOOM 的 hprof_strip.cpp 中实现。
koom 裁剪分析

2.分析 hprof 文件

有 haha 库或 shark 库可以很方便的分析 hprof 文件。
haha 分析 hprof 文件代码

  • 主要流程
    1.过滤重复的 GcRoot
    2.从快照中获取泄露的 act 实例,没有拿到就说明没有泄露
    3.广度遍历 GcRoot 的引用链,查看是否有与之匹配的 act 实例
// 开始分析的入口点在 HeapAnalyzerService
//HeapAnalyze
public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
    @NonNull String referenceKey,
    boolean computeRetainedSize) {
  long analysisStartNanoTime = System.nanoTime();
    ...
  try {
    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);
      // 过滤重复的 GcRoot
    deduplicateGcRoots(snapshot);
    listener.onProgressUpdate(FINDING_LEAKING_REF);
      // 拿到泄露 obj 在快照中的实例
    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));
    }
      // 广度遍历 GcRoot,查找泄露 leakingRef 与之遍历的 node 是否匹配
    return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
  } catch (Throwable e) {
    return failure(e, since(analysisStartNanoTime));
  }
}

    // 过滤重复的 GcRoot
  void deduplicateGcRoots(Snapshot snapshot) {
    // THashMap has a smaller memory footprint than 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) {
      // KeyedWeakReference 包裹 activity 的类
    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);
        // 找到泄露 act 
      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);
      //广度遍历 GcRoot,查找泄露 leakingRef 与之遍历的 node 是否匹配
    ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

      ...
      // 拿到泄露的路径
    LeakTrace leakTrace = buildLeakTrace(result.leakingNode);
      // 返回
    return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
        since(analysisStartNanoTime));
  }

  Result findPath(Snapshot snapshot, Instance leakingRef) {
    clearState();
    canIgnoreStrings = !isString(leakingRef);
    // GcRoot 入 toVisitQueue 队列,之后遍历
    enqueueGcRoots(snapshot);

    boolean excludingKnownLeaks = false;
    LeakNode leakingNode = null;
    while (!toVisitQueue.isEmpty() || !toVisitIfNoPathQueue.isEmpty()) {
      LeakNode node;
      if (!toVisitQueue.isEmpty()) {
          // 取出队列头
        node = toVisitQueue.poll();
      } else {...}

      // Termination
        // 找到了
      if (node.instance == leakingRef) {
        leakingNode = node;
        break;
      }

        // 将 node 的子节点入 toVisitQueue 队列,之后遍历
      if (node.instance instanceof RootObj) {
        visitRootObj(node);
      } else if (node.instance instanceof ClassObj) {
        visitClassObj(node);
      } else if (node.instance instanceof ClassInstance) {
        visitClassInstance(node);
      } else if (node.instance instanceof ArrayInstance) {
        visitArrayInstance(node);
      } else {
        throw new IllegalStateException("Unexpected type for " + node.instance);
      }
    }
    return new Result(leakingNode, excludingKnownLeaks);
  }

  private void enqueueGcRoots(Snapshot snapshot) {
      //遍历快照中所有的 GcRoot
    for (RootObj rootObj : HahaSpy.allGcRoots(snapshot)) {
      switch (rootObj.getRootType()) {
        case JAVA_LOCAL:
              ...
          break;
        case INTERNED_STRING:
        case DEBUGGER:
        case INVALID_TYPE:
          // An object that is unreachable from any other root, but not a root itself.
        case UNREACHABLE:
        case UNKNOWN:
          // An object that is in a queue, waiting for a finalizer to run.
        case FINALIZING:
          break;
        case SYSTEM_CLASS:
        case VM_INTERNAL:
          // A local variable in native code.
        case NATIVE_LOCAL:
          // A global variable in native code.
        case NATIVE_STATIC:
          // An object that was referenced from an active thread block.
        case THREAD_BLOCK:
          // Everything that called the wait() or notify() methods, or that is synchronized.
        case BUSY_MONITOR:
        case NATIVE_MONITOR:
        case REFERENCE_CLEANUP:
          // Input or output parameters in native code.
        case NATIVE_STACK:
        case JAVA_STATIC:
          enqueue(null, null, rootObj, null);
          break;
        default:
          throw new UnsupportedOperationException("Unknown root type:" + rootObj.getRootType());
      }
    }
  }

  private void enqueue(Exclusion exclusion, LeakNode parent, Instance child,
      LeakReference leakReference) {
    if (child == null) {
      return;
    }
      ...
    LeakNode childNode = new LeakNode(exclusion, child, parent, leakReference);
    if (visitNow) {
      toVisitSet.add(child);
      toVisitQueue.add(childNode);
    } else {
      toVisitIfNoPathSet.add(child);
      toVisitIfNoPathQueue.add(childNode);
    }
  }

// visitClassObj visitClassObj visitClassInstance visitArrayInstance 类似
  private void visitRootObj(LeakNode node) {
    RootObj rootObj = (RootObj) node.instance;
    Instance child = rootObj.getReferredInstance();

    if (rootObj.getRootType() == RootType.JAVA_LOCAL) {...} else {
      enqueue(null, node, child, null);
    }
  }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容