在对 hprof 文件进行处理前,首先需要对 hprof 文件格式有所了解。Android dump 的 hprof 文件和 java 的有一点点不一样,它在 java hprof 文件的基础上增加了几项,但文件格式是一样。所以需要先介绍下 java 的 hprof 文件格式,再介绍怎么处理 hprof 文件
在 java 中,hprof 文件有2部分组成,一部分是 hprof head,一部分是 hprof body。其中 head 比较简单,由版本号,IDSize和时间组成。

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

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

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 解析举个例子。

//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 文件,保留分析 OOM 的数据,使得在手机上的文件更容易的上传到后台分析。二是直接在手机上对 hprof 文件进行分析,中有用的信息。
hprof 文件中绝大部分数据是 PRIMITIVE ARRAY DUMP,通常占据80%以上,而分析 OOM 只关系对象的大小和引用关系,并不关心内容,因此这部分是裁剪的突破口。
源码在 KOOM 的 hprof_strip.cpp 中实现。
koom 裁剪分析
有 haha 库或 shark 库可以很方便的分析 hprof 文件。
haha 分析 hprof 文件代码
// 开始分析的入口点在 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);
}
}
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,
作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚
Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信