一.解决GC卡顿
为什么LeakCanary需要主动触发GC呢?LeakCanary监控泄漏利用了弱引用的特性,为Activity创建弱引用,当Activity对象变成弱可达时(没有强引用),弱引用会被加入到引用队列中,通过在Activity.onDestroy()后连续触发两次GC,并检查引用队列,可以判定Activity是否发生了泄漏。但频繁的GC会造成用户可感知的卡顿,为解决这一问题,我们设计了全新的监控模块,通过无性能损耗的内存阈值监控来触发镜像采集,具体策略如下:
具体策略如下:
- Java堆内存突破阈值触发采集 - 90%
- Java堆上涨速度突破阈值触发采集 - 两次检测时间间隔内增加350M直接dump
-
如何获取堆信息通过Runtime.getRuntime()获取 javaHeap.max = Runtime.getRuntime().maxMemory() javaHeap.total = Runtime.getRuntime().totalMemory() javaHeap.free = Runtime.getRuntime().freeMemory() javaHeap.used = javaHeap.total - javaHeap.free javaHeap.rate = 1.0f * javaHeap.used / javaHeap.max
-
-
处于高位heapThreshold,并且一直处于高位降的不明显 heapThreshold取值如下 maxMem >= 512 - 10 -> 0.8f maxMem >= 256 - 10 -> 0.85f else -> 0.9f - Java堆线程数突破阈值触发采集
-
线程阈值默认值 private val DEFAULT_THREAD_THRESHOLD by lazy { if (MonitorBuildConfig.ROM == "EMUI" && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) { 450 } else { 750 } } -
当前线程数量,读取"/proc/self/status"文件获取线程信息 File("/proc/self/status").forEachLineQuietly { line -> ... when { ... line.startsWith("Threads") -> { procStatus.thread = THREADS_REGEX.matchValue(line) } } }
-
- 文件描述符数突破阈值触发采集 - 1000
-
当前fd数量 private fun getFdCount(): Int { return File("/proc/self/fd").listFiles()?.size ?: 0 }
-
二.解决Dump hprof冻结app
Dump hprof是通过API Debug.dumpHprofData实现的,这个过程会**“冻结”**整个应用进程,造成数秒甚至数十秒内用户无法操作,这也是LeakCanary无法线上部署的最主要原因,如果能将这一过程优化至用户无感知,将会给OOM治理带来很大的想象空间。
面对这样一个问题,我们将其拆解,自然而然产生2个疑问:
1.为什么dumpHprofData会冻结app,虚拟机的实现原理是什么?
2.这个过程能异步吗?
我们来看dumpHprofData的虚拟机内部实现
art/runtime/hprof/hprof.cc
// If "direct_to_ddms" is true, the other arguments are ignored, and data is
// sent directly to DDMS.
// If "fd" is >= 0, the output will be written to that file descriptor.
// Otherwise, "filename" is used to create an output file.
void DumpHeap(const char* filename, int fd, bool direct_to_ddms) {
CHECK(filename != nullptr);
Thread* self = Thread::Current();
// Need to take a heap dump while GC isn't running. See the comment in Heap::VisitObjects().
// Also we need the critical section to avoid visiting the same object twice. See b/34967844
gc::ScopedGCCriticalSection gcs(self,
gc::kGcCauseHprof,
gc::kCollectorTypeHprof);
ScopedSuspendAll ssa(__FUNCTION__, true /* long suspend */);
Hprof hprof(filename, fd, direct_to_ddms);
hprof.Dump();
}
可以看到在dump前,通过ScopedSuspendAll(构造函数中执行SuspendAll)执行了暂停所有java线程的操作,以防止在dump的过程中java堆发生变化,当dump结束后通过ScopedSuspendAll析构函数进行ResumeAll。
解决了第一个问题,接下来看第二个问题,既然要冻结所有线程,子线程异步处理是没有意义的,那么在子进程中处理呢?Android的内核是定制过的Linux, 而Linux fork子进程有一个著名的COW(Copy-on-write,写时复制)机制,即为了节省fork子进程的内存消耗和耗时,fork出的子进程并不会copy父进程的内存,而是和父进程共享内存空间。那么如何做到进程隔离呢,父子进程只在发生内存写入操作时,系统才会分配新的内存为写入方保留单独的拷贝,这就相当于子进程保留了fork瞬间时父进程的内存镜像,且后续父进程对内存的修改不会影响子进程,想到这里我们豁然开朗。说干就干,我们写了一个demo来验证这个思路,
很快就遇到了棘手的新问题:dump前需要暂停所有java线程,而子进程只保留父进程执行fork操作的线程,在子进程中执行SuspendAll触发暂停是永远等不到其他线程返回结果的(详见thread_list.cc中行SuspendAll的实现,这里不展开讲了),经过仔细分析SuspendAll的过程,我们发现,可以先在主进程执行SuspendAll,使ThreadList中保存的所有线程状态为suspend,之后fork,子进程共享父进程的ThreadList全局变量,子进程可以欺骗虚拟机,使其以为子进程全部线程已经完成了暂停操作,接下来子进程就可以愉快的dump hprof了,而父进程可以立刻执行ResumeAll恢复运行。
这里有一个小技巧,SuspendAll没有对外暴露Java层的API,我们可以通过C层间接暴露的art::Dbg::SuspendVM来调用,dlsym拿到“_ZN3art3Dbg9SuspendVMEv”的地址调用即可,ResumeAll同理,注意这个函数在android 11以后已经被去除了,需要另行适配。Android 7之后对linker做了限制(即dlopen系统库失效),快手自研了kwai-linker组件,通过caller address替换和dl_iterate_phdr解析绕过了这一限制。
至此,我们完美解决了dump hprof冻结app的问题,用一张图总结:

1.ForkJvmHeapDumper fork dump流程
//topic 如何开辟子进程dump的 step1
ForkJvmHeapDumper().run {
dump(hprofFile.absolutePath)//开辟子进程dump
}
public class ForkJvmHeapDumper extends HeapDumper {
...
@Override
public boolean dump(String path) {
MonitorLog.i(TAG, "dump " + path);
...
boolean dumpRes = false;
try {
MonitorLog.i(TAG, "before suspend and fork.");
//topic 如何开辟子进程dump的 step3-1
int pid = suspendAndFork();//pid为0,开辟的子进程,将去dump
if (pid == 0) {
// Child process
Debug.dumpHprofData(path);
exitProcess();//topic 如何开辟子进程dump的 step3-2
} else if (pid > 0) {//主进程将会resumeAndWait
// Parent process
dumpRes = resumeAndWait(pid);//topic 如何开辟子进程dump的 step3-3
MonitorLog.i(TAG, "notify from pid " + pid);
}
} catch (IOException e) {
MonitorLog.e(TAG, "dump failed caused by " + e.toString());
e.printStackTrace();
}
return dumpRes;
}
/**
* Init before do dump. //topic 如何开辟子进程dump的 step2
*/
private native void init();
/**
* Suspend the whole ART, and then fork a process for dumping hprof.
*
* @return return value of fork
*/
private native int suspendAndFork();//topic 如何开辟子进程dump的 step3-1
/**
* Resume the whole ART, and then wait child process to notify.
*
* @param pid pid of child process.
*/
private native boolean resumeAndWait(int pid);//topic 如何开辟子进程dump的 step3-3
/**
* Exit current process.
*/
private native void exitProcess();//topic 如何开辟子进程dump的 step3-2
}
2.hrof_dump.h 代码分析
#ifndef KOOM_HPROF_DUMP_H
#define KOOM_HPROF_DUMP_H
#include <android-base/macros.h>
#include <memory>
#include <string>
namespace kwai {
namespace leak_monitor {
...
class HprofDump {
public:
//获取HprofDump实例
static HprofDump &GetInstance();
//初始化
void Initialize();
//SuspendAndFork
pid_t SuspendAndFork();
//ResumeAndWait
bool ResumeAndWait(pid_t pid);
private:
HprofDump();
~HprofDump() = default;
//https://blog.youkuaiyun.com/u011157036/article/details/45247965
//有时候,进行类体设计时,会发现某个类的对象是独一无二的,没有完全相同的对象,也就是对该类对象做副本没有任何意义.
//因此,需要限制编译器自动生动的拷贝构造函数和赋值构造函数.一般参用下面的宏定义的方式进行限制,代码如下:
DISALLOW_COPY_AND_ASSIGN(HprofDump);
//初始化完成
bool init_done_;
//api版本
int android_api_;
/**
* Function pointer for ART <= Android Q
* 方法指针 ART小于等于Android q的
*/
//suspend vm的方法
// art::Dbg::SuspendVM
void (*suspend_vm_fnc_)();
//resume vm的方法
// art::Dbg::ResumeVM
void (*resume_vm_fnc_)();
/**
* Function pointer for ART Android R
* todo 方法指针 art android R的忽略先
*/
};
} // namespace leak_monitor
} // namespace kwai
#endif // KOOM_HPROF_DUMP_H
3.hprof_dump.cpp 原理
#undef LOG_TAG
#define LOG_TAG "HprofDump"
using namespace kwai::linker;
namespace kwai {
namespace leak_monitor {
......
//初始化
void HprofDump::Initialize() {
if (init_done_ || android_api_ < __ANDROID_API_L__) {
return;
}
//获取libart的handle手柄,把手
void *handle = kwai::linker::DlFcn::dlopen("libart.so", RTLD_NOW);
KCHECKV(handle)
if (android_api_ < __ANDROID_API_R__) {
//获取SuspendVMEv方法指针
suspend_vm_fnc_ =
(void (*)()) DlFcn::dlsym(handle, "_ZN3art3Dbg9SuspendVMEv");
KFINISHV_FNC(suspend_vm_fnc_, DlFcn::dlclose, handle)
//获取ResumeVMEv方法指针,resume vm的方法
resume_vm_fnc_ = (void (*)()) kwai::linker::DlFcn::dlsym(
handle, "_ZN3art3Dbg8ResumeVMEv");
KFINISHV_FNC(resume_vm_fnc_, DlFcn::dlclose, handle)
}
if (android_api_ == __ANDROID_API_R__) {
//R 的忽略先...
}
DlFcn::dlclose(handle);
init_done_ = true;
}
pid_t HprofDump::SuspendAndFork() {
KCHECKI(init_done_)
if (android_api_ < __ANDROID_API_R__) {
suspend_vm_fnc_(); //suspend虚拟机
}
if (android_api_ == __ANDROID_API_R__) {
//R 的忽略先...
}
pid_t pid = fork(); //fork子进程
if (pid == 0) { //如果是子进程
// Set timeout for child process
alarm(60); //子进程60s之内退出
prctl(PR_SET_NAME, "forked-dump-process");//设置子进程名字
}
return pid;
}
bool HprofDump::ResumeAndWait(pid_t pid) {
KCHECKB(init_done_) //检测init_done_是否true,如果不是true,return;
if (android_api_ < __ANDROID_API_R__) {
resume_vm_fnc_(); //resume vm的方法
}
if (android_api_ == __ANDROID_API_R__) {
//R 的忽略先...
}
int status;
for (;;) {
if (waitpid(pid, &status, 0) != -1 || errno != EINTR) {
//WIFEXITED 函数作用 Returns true if the process exited normally.
//status为0正常退出,dump成功
if (!WIFEXITED(status)) {
ALOGE("Child process %d exited with status %d, terminated by signal %d",
pid, WEXITSTATUS(status), WTERMSIG(status));
return false;
}
return true;
}
return false;
}
}
} // namespace leak_monitor
} // namespace kwai
三.解决hprof文件过大
我们将问题拆解:
- hprof存的内容都是些什么?数据如何组织的?哪些可以裁掉?
- 内存中的数据结构和hprof文件二进制协议的映射关系?
- 如何裁剪?
想要了解hprof的数据组织方式,这里简要介绍一下核心内容:
文件按byte by byte顺序存储,u1,u2,u4分别代表1字节,2字节,4字节。
总体分为两部分,Header和Record,Header记录hprof的元信息,Record分很多条目,每一条有一个单独的TAG代表类型。
我们关注的Record类型主要是HEAP DUMP,其中又分五个子类,分别为GC ROOT、CLASS DUMP、INSTANCE DUMP、OBJECT ARRAY DUMP、PRIMITIVE ARRAY DUMP。图13以PRIMITIVE ARRAY DUMP(基本类型数组)为例展示Record中包含的信息,其他类型请查阅官方文档。内存中绝大部分数据是PRIMITIVE ARRAY DUMP,通常占据80%以上,而我们分析OOM只关系对象的大小和引用关系,并不关心内容,因此这部分是我们裁剪的突破口。

Android对数据类型做了扩展,增加了一些GC ROOT
// Android.
HPROF_HEAP_DUMP_INFO = 0xfe,
HPROF_ROOT_INTERNED_STRING = 0x89,
HPROF_ROOT_FINALIZING = 0x8a, // Obsolete.
HPROF_ROOT_DEBUGGER = 0x8b,
HPROF_ROOT_REFERENCE_CLEANUP = 0x8c, // Obsolete.
HPROF_ROOT_VM_INTERNAL = 0x8d,
HPROF_ROOT_JNI_MONITOR = 0x8e,
HPROF_UNREACHABLE = 0x90, // Obsolete.
HPROF_PRIMITIVE_ARRAY_NODATA_DUMP = 0xc3, // Obsolete.
还有一个HEAP_DUMP_INFO,这里面保存的是堆空间(heap space)的类型,Android对堆空间做了划分,我们只关注HPROF_HEAP_APP即可,其余也是可以裁剪掉的,可以参考Android Studio中Memory Profiler的处理[3]。
enum HprofHeapId {
HPROF_HEAP_DEFAULT = 0,
HPROF_HEAP_ZYGOTE = 'Z',
HPROF_HEAP_APP = 'A',
HPROF_HEAP_IMAGE = 'I',
};
接下来讨论如何裁剪,裁剪有两种办法,第一种是在dump完成后的hprof文件基础上裁剪,性能比较差,对磁盘空间要求也比较高,第二种是在dump的过程中实时裁剪,我们自然想要实现第二种。看一下Record写入的过程,先执行StartNewRecord,然后通过AddU1/U4/U8写入内存buffer,最后执行EndRecord将buffer写入文件。
void StartNewRecord(uint8_t tag, uint32_t time) {
if (length_ > 0) {
EndRecord();
}
DCHECK_EQ(length_, 0U);
AddU1(tag);
AddU4(time);
AddU4(0xdeaddead); // Length, replaced on flush.
started_ = true;
}
void EndRecord() {
// Replace length in header.
if (started_) {
UpdateU4(sizeof(uint8_t) + sizeof(uint32_t),
length_ - sizeof(uint8_t) - 2 * sizeof(uint32_t));
}
HandleEndRecord();
sum_length_ += length_;
max_length_ = std::max(max_length_, length_);
length_ = 0;
started_ = false;
}
void HandleFlush(const uint8_t* buffer, size_t length) override {
if (!errors_) {
errors_ = !fp_->WriteFully(buffer, length);
}
}
这个过程中有两个hook点可以选择,一是hook AddUx,在写入buffer的过程中裁剪,二是hook write,在写入文件过程中裁剪。最终我们选择了方案二,理由是AddUx调用比较频繁,判断逻辑复杂容易出现兼容性问题,而write是public API,且只在Record写入文件的时候调用一次,厂商不会魔改相关实现,从hook原理上来讲,hook外部调用的PLT/GOT hook也比hook内部调用的inline hook要稳定得多。
用一张图总结裁剪的流程:

1.hprof_strip.h 源码解析
#ifndef KOOM_HPROF_STRIP_H
#define KOOM_HPROF_STRIP_H
#include <android-base/macros.h>
#include <memory>
#include <string>
namespace kwai {
namespace leak_monitor {
class HprofStrip {
public:
//获取HprofStrip实例
static HprofStrip &GetInstance();
//init方法
static void HookInit();
//hook open的方法,flags todo
int HookOpenInternal(const char *path_name, int flags, ...);
//hook write的方法
/**
*
* @param fd 文件描述符
* @param buf 一段内存,开始指针
* @param count 这段内存字节数
* @return 看着返回也是count
*/
ssize_t HookWriteInternal(int fd, const void *buf, size_t count);
//是否hook成功
bool IsHookSuccess() const;
//设置hprof文件名字
void SetHprofName(const char *hprof_name);
private:
//构造函数
HprofStrip();
//析构函数
~HprofStrip() = default;
//https://blog.youkuaiyun.com/u011157036/article/details/45247965
//有时候,进行类体设计时,会发现某个类的对象是独一无二的,没有完全相同的对象,也就是对该类对象做副本没有任何意义.
//因此,需要限制编译器自动生动的拷贝构造函数和赋值构造函数.一般参用下面的宏定义的方式进行限制,代码如下:
DISALLOW_COPY_AND_ASSIGN(HprofStrip);
//从buf的index位置获取short
static int GetShortFromBytes(const unsigned char *buf, int index);
//从buf的index位置获取int
static int GetIntFromBytes(const unsigned char *buf, int index);
//获取相关类型占多少字节
static int GetByteSizeFromType(unsigned char basic_type);
/**
* 递归处理一段buf,按tag来处理
* @param buf 处理的数据指针
* @param first_index 开始处理位置
* @param max_len 这段数据的字节数
* @param heap_serial_no是 heap_serial_num_当前的值,我们关注的Record类型主要是HEAP DUMP,
* heap_serial_num_表示HEAP DUMP的数量
* @param array_serial_no 处理的基本类型数组的个数,基本类型数组tag是HPROF_PRIMITIVE_ARRAY_DUMP
* @return
*/
int ProcessHeap(const void *buf, int first_index, int max_len,
int heap_serial_no, int array_serial_no);
//重置
void reset();
//文件描述符
int hprof_fd_;
//裁剪字节计数
int strip_bytes_sum_;
//我们关注的Record类型主要是HEAP DUMP,其中又分五个子类,分别为GC ROOT、CLASS DUMP、INSTANCE DUMP、OBJECT ARRAY DUMP、PRIMITIVE ARRAY DUMP。
//HPROF_TAG_HEAP_DUMP HPROF_TAG_HEAP_DUMP_SEGMENT 的个数
int heap_serial_num_;
//hook的write调用了多少次
int hook_write_serial_num_;
//裁剪次数计数,和strip_index_list_pair_数组结合使用
int strip_index_;
//是否hook成功
bool is_hook_success_;
//是否是系统heap,heap_type == HPROF_HEAP_ZYGOTE || heap_type == HPROF_HEAP_IMAGE,这俩需要裁剪
bool is_current_system_heap_;
//hprof名字
std::string hprof_name_;
//Strip裁剪区域数组大小 2^16 * 2 * 2 + 2
static constexpr int kStripListLength = 65536 * 2 * 2 + 2;
//每两个为一组,第一个值为开始位置,第二个值为结束的位置,记录裁剪区域
int strip_index_list_pair_[kStripListLength];
};
} // namespace leak_monitor
} // namespace kwai
#endif // KOOM_HPROF_STRIP_H
2.hprof_strip.cpp 裁剪源码分析
#include <android/log.h>
#include <fcntl.h>
#include <hprof_strip.h>
#include <kwai_util/kwai_macros.h>
#include <unistd.h>
#include <xhook.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <list>
#define LOG_TAG "HprofCrop"
namespace kwai {
namespace leak_monitor {
enum HprofTag {
HPROF_TAG_STRING = 0x01,
HPROF_TAG_LOAD_CLASS = 0x02,
HPROF_TAG_UNLOAD_CLASS = 0x03,
HPROF_TAG_STACK_FRAME = 0x04,
HPROF_TAG_STACK_TRACE = 0x05,
HPROF_TAG_ALLOC_SITES = 0x06,
HPROF_TAG_HEAP_SUMMARY = 0x07,
HPROF_TAG_START_THREAD = 0x0A,
HPROF_TAG_END_THREAD = 0x0B,
HPROF_TAG_HEAP_DUMP = 0x0C, //关注的Record类型主要是HEAP DUMP
HPROF_TAG_HEAP_DUMP_SEGMENT = 0x1C, //关注的Record类型主要是HEAP DUMP
HPROF_TAG_HEAP_DUMP_END = 0x2C,
HPROF_TAG_CPU_SAMPLES = 0x0D,
HPROF_TAG_CONTROL_SETTINGS = 0x0E,
};
enum HprofHeapTag {
// Traditional.
HPROF_ROOT_UNKNOWN = 0xFF,
HPROF_ROOT_JNI_GLOBAL = 0x01,
HPROF_ROOT_JNI_LOCAL = 0x02,
HPROF_ROOT_JAVA_FRAME = 0x03,
HPROF_ROOT_NATIVE_STACK = 0x04,
HPROF_ROOT_STICKY_CLASS = 0x05,
HPROF_ROOT_THREAD_BLOCK = 0x06,
HPROF_ROOT_MONITOR_USED = 0x07,
HPROF_ROOT_THREAD_OBJECT = 0x08,
HPROF_CLASS_DUMP = 0x20,
HPROF_INSTANCE_DUMP = 0x21,
HPROF_OBJECT_ARRAY_DUMP = 0x22,
HPROF_PRIMITIVE_ARRAY_DUMP = 0x23,
// Android.
HPROF_HEAP_DUMP_INFO = 0xfe,
HPROF_ROOT_INTERNED_STRING = 0x89,
HPROF_ROOT_FINALIZING = 0x8a, // Obsolete.
HPROF_ROOT_DEBUGGER = 0x8b,
HPROF_ROOT_REFERENCE_CLEANUP = 0x8c, // Obsolete.
HPROF_ROOT_VM_INTERNAL = 0x8d,
HPROF_ROOT_JNI_MONITOR = 0x8e,
HPROF_UNREACHABLE = 0x90, // Obsolete.
HPROF_PRIMITIVE_ARRAY_NODATA_DUMP = 0xc3, // Obsolete.
};
enum HprofBasicType {
hprof_basic_object = 2,
hprof_basic_boolean = 4,
hprof_basic_char = 5,
hprof_basic_float = 6,
hprof_basic_double = 7,
hprof_basic_byte = 8,
hprof_basic_short = 9,
hprof_basic_int = 10,
hprof_basic_long = 11,
};
enum HprofHeapId {
HPROF_HEAP_DEFAULT = 0,
HPROF_HEAP_ZYGOTE = 'Z',
HPROF_HEAP_APP = 'A',
HPROF_HEAP_IMAGE = 'I',
};
enum HprofTagBytes {
OBJECT_ID_BYTE_SIZE = 4,
JNI_GLOBAL_REF_ID_BYTE_SIZE = 4,
CLASS_ID_BYTE_SIZE = 4,
CLASS_LOADER_ID_BYTE_SIZE = 4,
INSTANCE_SIZE_BYTE_SIZE = 4,
CONSTANT_POOL_LENGTH_BYTE_SIZE = 2,
STATIC_FIELD_LENGTH_BYTE_SIZE = 2,
INSTANCE_FIELD_LENGTH_BYTE_SIZE = 2,
STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE = 4,
RECORD_TIME_BYTE_SIZE = 4,
RECORD_LENGTH_BYTE_SIZE = 4,
STRING_ID_BYTE_SIZE = 4,
HEAP_TAG_BYTE_SIZE = 1,
THREAD_SERIAL_BYTE_SIZE = 4,
CONSTANT_POLL_INDEX_BYTE_SIZE = 2,
BASIC_TYPE_BYTE_SIZE = 1,
HEAP_TYPE_BYTE_SIZE = 4,
};
static constexpr int U4 = 4;
//从buf的index位置获取short
ALWAYS_INLINE int HprofStrip::GetShortFromBytes(const unsigned char *buf,
int index) {
return (buf[index] << 8u) + buf[index + 1];
}
//从buf的index位置获取int
ALWAYS_INLINE int HprofStrip::GetIntFromBytes(const unsigned char *buf,
int index) {
return (buf[index] << 24u) + (buf[index + 1] << 16u) +
(buf[index + 2] << 8u) + buf[index + 3];
}
//获取相关类型占多少字节
int HprofStrip::GetByteSizeFromType(unsigned char basic_type) {
switch (basic_type) {
case hprof_basic_boolean:
case hprof_basic_byte:
return 1;
case hprof_basic_char:
case hprof_basic_short:
return 2;
case hprof_basic_float:
case hprof_basic_int:
case hprof_basic_object:
return 4;
case hprof_basic_long:
case hprof_basic_double:
return 8;
default:
return 0;
}
}
/**
* 递归处理一段buf,按tag来处理
* @param buf 处理的数据指针
* @param first_index 开始处理位置
* @param max_len 这段数据的字节数
* @param heap_serial_no是 heap_serial_num_当前的值,我们关注的Record类型主要是HEAP DUMP,
* heap_serial_num_表示HEAP DUMP的数量
* @param array_serial_no 表示处理的基本类型数组的个数,基本类型数组tag是HPROF_PRIMITIVE_ARRAY_DUMP
* @return
*/
int HprofStrip::ProcessHeap(const void *buf, int first_index, int max_len,
int heap_serial_no, int array_serial_no) {
//到达最后一个位置就返回array_serial_no,表示处理的基本类型数组的个数
//基本类型数组tag是HPROF_PRIMITIVE_ARRAY_DUMP
if (first_index >= max_len) {
return array_serial_no;
}
const unsigned char subtag = ((unsigned char *) buf)[first_index];
//我们关注的Record类型主要是HEAP DUMP,其中又分五个子类,
// 分别为GC ROOT、CLASS DUMP、INSTANCE DUMP、OBJECT ARRAY DUMP、PRIMITIVE ARRAY DUMP。
switch (subtag) {
/**
* __ AddU1(heap_tag);
* __ AddObjectId(obj);
*
*/
case HPROF_ROOT_UNKNOWN:
case HPROF_ROOT_STICKY_CLASS:
case HPROF_ROOT_MONITOR_USED:
case HPROF_ROOT_INTERNED_STRING:
case HPROF_ROOT_DEBUGGER:
case HPROF_ROOT_VM_INTERNAL: {
//递归处理,first_index变为first_index+AddU1+AddObjectId
array_serial_no = ProcessHeap(
buf, first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE, max_len,
heap_serial_no, array_serial_no);
}
break;
case HPROF_ROOT_JNI_GLOBAL: {
/**
* __ AddU1(heap_tag);
* __ AddObjectId(obj);
* __ AddJniGlobalRefId(jni_obj);
*
*/
//递归处理,first_index变为first_index+AddU1+AddObjectId+AddJniGlobalRefId
array_serial_no =
ProcessHeap(buf,
first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
JNI_GLOBAL_REF_ID_BYTE_SIZE,
max_len, heap_serial_no, array_serial_no);
}
break;
/**
* __ AddU1(heap_tag);
* __ AddObjectId(obj);
* __ AddU4(thread_serial);
* __ AddU4((uint32_t)-1);
*/
case HPROF_ROOT_JNI_LOCAL:
case HPROF_ROOT_JAVA_FRAME:
case HPROF_ROOT_JNI_MONITOR: {
//递归处理,first_index变为first_index+AddU1+AddObjectId+AddU4+AddU4
array_serial_no =
ProcessHeap(buf,
first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
THREAD_SERIAL_BYTE_SIZE + U4 /*占位*/,
max_len, heap_serial_no, array_serial_no);
}
break;
/**
* __ AddU1(heap_tag);
* __ AddObjectId(obj);
* __ AddU4(thread_serial);
*/
case HPROF_ROOT_NATIVE_STACK:
case HPROF_ROOT_THREAD_BLOCK: {
//递归处理,first_index变为first_index+AddU1+AddObjectId+AddU4
array_serial_no =
ProcessHeap(buf,
first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
THREAD_SERIAL_BYTE_SIZE,
max_len, heap_serial_no, array_serial_no);
}
break;
/**
* __ AddU1(heap_tag);
* __ AddObjectId(obj);
* __ AddU4(thread_serial);
* __ AddU4((uint32_t)-1); // xxx
*/
case HPROF_ROOT_THREAD_OBJECT: {
//递归处理,first_index变为first_index+AddU1+AddObjectId+AddU4+AddU4
array_serial_no =
ProcessHeap(buf,
first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
THREAD_SERIAL_BYTE_SIZE + U4 /*占位*/,
max_len, heap_serial_no, array_serial_no);
}
break;
/**
* __ AddU1(HPROF_CLASS_DUMP);
* __ AddClassId(LookupClassId(klass));
* __ AddStackTraceSerialNumber(LookupStackTraceSerialNumber(klass));
* __ AddClassId(LookupClassId(klass->GetSuperClass().Ptr()));
* __ AddObjectId(klass->GetClassLoader().Ptr());
* __ AddObjectId(nullptr); // no signer
* __ AddObjectId(nullptr); // no prot domain
* __ AddObjectId(nullptr); // reserved
* __ AddObjectId(nullptr); // reserved
* __ AddU4(0); 或 __ AddU4(sizeof(mirror::String)); 或 __ AddU4(0); 或 __
* AddU4(klass->GetObjectSize()); // instance size
* __ AddU2(0); // empty const pool
* __ AddU2(dchecked_integral_cast<uint16_t>(static_fields_reported));
* static_field_writer(class_static_field, class_static_field_name_fn);
*/
case HPROF_CLASS_DUMP: {
/**
* u2
size of constant pool and number of records that follow:
u2
constant pool index
u1
type of entry: (See Basic Type)
value
value of entry (u1, u2, u4, or u8 based on type of entry)
*/
int constant_pool_index =
first_index + HEAP_TAG_BYTE_SIZE /*tag*/
+ CLASS_ID_BYTE_SIZE + STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE +
CLASS_ID_BYTE_SIZE /*super*/ + CLASS_LOADER_ID_BYTE_SIZE +
OBJECT_ID_BYTE_SIZE // Ignored: Signeres ID.
+ OBJECT_ID_BYTE_SIZE // Ignored: Protection domain ID.
+ OBJECT_ID_BYTE_SIZE // RESERVED.
+ OBJECT_ID_BYTE_SIZE // RESERVED.
+ INSTANCE_SIZE_BYTE_SIZE;
//读取常量大小
int constant_pool_size =
GetShortFromBytes((unsigned char *) buf, constant_pool_index);
constant_pool_index += CONSTANT_POOL_LENGTH_BYTE_SIZE;
for (int i = 0; i < constant_pool_size; ++i) {
unsigned char type = ((
unsigned char *) buf)[constant_pool_index +
CONSTANT_POLL_INDEX_BYTE_SIZE /*pool index*/];
constant_pool_index += CONSTANT_POLL_INDEX_BYTE_SIZE /*poll index*/
+ BASIC_TYPE_BYTE_SIZE /*type*/ +
GetByteSizeFromType(type);
}
/**
* u2 Number of static fields:
ID
static field name string ID
u1
type of field: (See Basic Type)
value
value of entry (u1, u2, u4, or u8 based on type of field)
*/
//读取static fields大小
int static_fields_index = constant_pool_index;
int static_fields_size =
GetShortFromBytes((unsigned char *) buf, static_fields_index);
static_fields_index += STATIC_FIELD_LENGTH_BYTE_SIZE;
for (int i = 0; i < static_fields_size; ++i) {
unsigned char type =
((unsigned char *)
buf)[static_fields_index + STRING_ID_BYTE_SIZE /*ID*/];
static_fields_index += STRING_ID_BYTE_SIZE /*string ID*/ +
BASIC_TYPE_BYTE_SIZE /*type*/
+ GetByteSizeFromType(type);
}
/**
* u2
Number of instance fields (not including super class's)
ID
field name string ID
u1
type of field: (See Basic Type)
*/
int instance_fields_index = static_fields_index;
//获取实例
int instance_fields_size =
GetShortFromBytes((unsigned char *) buf, instance_fields_index);
instance_fields_index += INSTANCE_FIELD_LENGTH_BYTE_SIZE;
instance_fields_index +=
(BASIC_TYPE_BYTE_SIZE + STRING_ID_BYTE_SIZE) * instance_fields_size;
array_serial_no = ProcessHeap(buf, instance_fields_index, max_len,
heap_serial_no, array_serial_no);
}
break;
/**
*__ AddU1(HPROF_INSTANCE_DUMP);
* __ AddObjectId(obj);
* __ AddStackTraceSerialNumber(LookupStackTraceSerialNumber(obj));
* __ AddClassId(LookupClassId(klass));
*
* __ AddU4(0x77777777);//length
*
* ***
*/
case HPROF_INSTANCE_DUMP: {
int instance_dump_index =
first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE + CLASS_ID_BYTE_SIZE;
int instance_size =
GetIntFromBytes((unsigned char *) buf, instance_dump_index);
// 裁剪掉system space
if (is_current_system_heap_) {
strip_index_list_pair_[strip_index_ * 2] = first_index;
strip_index_list_pair_[strip_index_ * 2 + 1] =
instance_dump_index + U4 /*占位*/ + instance_size;
strip_index_++;
strip_bytes_sum_ +=
instance_dump_index + U4 /*占位*/ + instance_size - first_index;
}
array_serial_no =
ProcessHeap(buf, instance_dump_index + U4 /*占位*/ + instance_size,
max_len, heap_serial_no, array_serial_no);
}
break;
/**
* __ AddU1(HPROF_OBJECT_ARRAY_DUMP);
* __ AddObjectId(obj);
* __ AddStackTraceSerialNumber(LookupStackTraceSerialNumber(obj));
* __ AddU4(length);
* __ AddClassId(LookupClassId(klass));
*
* // Dump the elements, which are always objects or null.
* __ AddIdList(obj->AsObjectArray<mirror::Object>().Ptr());
*/
case HPROF_OBJECT_ARRAY_DUMP: {
int length = GetIntFromBytes((unsigned char *) buf,
first_index + HEAP_TAG_BYTE_SIZE +
OBJECT_ID_BYTE_SIZE +
STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE);
// 裁剪掉system space
if (is_current_system_heap_) {
strip_index_list_pair_[strip_index_ * 2] = first_index;
strip_index_list_pair_[strip_index_ * 2 + 1] =
first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE + U4 /*Length*/
+ CLASS_ID_BYTE_SIZE + U4 /*Id*/ * length;
strip_index_++;
strip_bytes_sum_ += HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE + U4 /*Length*/
+ CLASS_ID_BYTE_SIZE + U4 /*Id*/ * length;
}
array_serial_no =
ProcessHeap(buf,
first_index + HEAP_TAG_BYTE_SIZE + OBJECT_ID_BYTE_SIZE +
STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE + U4 /*Length*/
+ CLASS_ID_BYTE_SIZE + U4 /*Id*/ * length,
max_len, heap_serial_no, array_serial_no);
}
break;
/**
*
* __ AddU1(HPROF_PRIMITIVE_ARRAY_DUMP);
* __ AddClassStaticsId(klass);
* __ AddStackTraceSerialNumber(LookupStackTraceSerialNumber(klass));
* __ AddU4(java_heap_overhead_size - 4);
* __ AddU1(hprof_basic_byte);
* for (size_t i = 0; i < java_heap_overhead_size - 4; ++i) {
* __ AddU1(0);
* }
*
* // obj is a primitive array.
* __ AddU1(HPROF_PRIMITIVE_ARRAY_DUMP);
* __ AddObjectId(obj);
* __ AddStackTraceSerialNumber(LookupStackTraceSerialNumber(obj));
* __ AddU4(length);
* __ AddU1(t);
* // Dump the raw, packed element values.
* if (size == 1) {
* __ AddU1List(reinterpret_cast<const
* uint8_t*>(obj->GetRawData(sizeof(uint8_t), 0)), length); } else if
* (size == 2) {
* __ AddU2List(reinterpret_cast<const
* uint16_t*>(obj->GetRawData(sizeof(uint16_t), 0)), length); } else if
* (size == 4) {
* __ AddU4List(reinterpret_cast<const
* uint32_t*>(obj->GetRawData(sizeof(uint32_t), 0)), length); } else if
* (size == 8) {
* __ AddU8List(reinterpret_cast<const
* uint64_t*>(obj->GetRawData(sizeof(uint64_t), 0)), length);
* }
*/
case HPROF_PRIMITIVE_ARRAY_DUMP: {
int primitive_array_dump_index = first_index + HEAP_TAG_BYTE_SIZE /*tag*/
+ OBJECT_ID_BYTE_SIZE +
STACK_TRACE_SERIAL_NUMBER_BYTE_SIZE;
int length =
GetIntFromBytes((unsigned char *) buf, primitive_array_dump_index);
primitive_array_dump_index += U4 /*Length*/;
// 裁剪掉基本类型数组,无论是否在system space都进行裁剪
// 区别是数组左坐标,app space时带数组元信息(类型、长度)方便回填 todo
if (is_current_system_heap_) {
strip_index_list_pair_[strip_index_ * 2] = first_index;
} else {
strip_index_list_pair_[strip_index_ * 2] =
primitive_array_dump_index + BASIC_TYPE_BYTE_SIZE /*value type*/;
}
array_serial_no++;
int value_size = GetByteSizeFromType(
((unsigned char *) buf)[primitive_array_dump_index]);
primitive_array_dump_index +=
BASIC_TYPE_BYTE_SIZE /*value type*/ + value_size * length;
// 数组右坐标
strip_index_list_pair_[strip_index_ * 2 + 1] = primitive_array_dump_index;
// app space时,不修改长度因为回填数组时会补齐 todo
if (is_current_system_heap_) {
strip_bytes_sum_ += primitive_array_dump_index - first_index;
}
strip_index_++;
array_serial_no = ProcessHeap(buf, primitive_array_dump_index, max_len,
heap_serial_no, array_serial_no);
}
break;
// Android.
case HPROF_HEAP_DUMP_INFO: {
const unsigned char heap_type =
((unsigned char *) buf)[first_index + HEAP_TAG_BYTE_SIZE + 3];
is_current_system_heap_ =
(heap_type == HPROF_HEAP_ZYGOTE || heap_type == HPROF_HEAP_IMAGE);
if (is_current_system_heap_) {
strip_index_list_pair_[strip_index_ * 2] = first_index;
strip_index_list_pair_[strip_index_ * 2 + 1] =
first_index + HEAP_TAG_BYTE_SIZE /*TAG*/
+ HEAP_TYPE_BYTE_SIZE /*heap type*/
+ STRING_ID_BYTE_SIZE /*string id*/;
strip_index_++;
strip_bytes_sum_ += HEAP_TAG_BYTE_SIZE /*TAG*/
+ HEAP_TYPE_BYTE_SIZE /*heap type*/
+ STRING_ID_BYTE_SIZE /*string id*/;
}
array_serial_no = ProcessHeap(buf,
first_index + HEAP_TAG_BYTE_SIZE /*TAG*/
+ HEAP_TYPE_BYTE_SIZE /*heap type*/
+ STRING_ID_BYTE_SIZE /*string id*/,
max_len, heap_serial_no, array_serial_no);
}
break;
case HPROF_ROOT_FINALIZING: // Obsolete.
case HPROF_ROOT_REFERENCE_CLEANUP: // Obsolete.
case HPROF_UNREACHABLE: // Obsolete.
case HPROF_PRIMITIVE_ARRAY_NODATA_DUMP: { // Obsolete.
array_serial_no = ProcessHeap(buf, first_index + HEAP_TAG_BYTE_SIZE,
max_len, heap_serial_no, array_serial_no);
}
break;
default:
break;
}
return array_serial_no;
}
static int HookOpen(const char *pathname, int flags, ...) {
va_list ap;
va_start(ap, flags);
int fd = HprofStrip::GetInstance().HookOpenInternal(pathname, flags, ap);
va_end(ap);
return fd;
}
int HprofStrip::HookOpenInternal(const char *path_name, int flags, ...) {
va_list ap;
va_start(ap, flags);
int fd = open(path_name, flags, ap);
va_end(ap);
if (hprof_name_.empty()) { //hprof_name_是我们的hprof
return fd;
}
if (path_name != nullptr && strstr(path_name, hprof_name_.c_str())) {
hprof_fd_ = fd; //设置hprof_fd_
is_hook_success_ = true; //设置is_hook_success_为true
}
return fd;
}
static ssize_t HookWrite(int fd, const void *buf, size_t count) {
return HprofStrip::GetInstance().HookWriteInternal(fd, buf, count);
}
void HprofStrip::reset() {
strip_index_ = 0;
strip_bytes_sum_ = 0;
}
ssize_t HprofStrip::HookWriteInternal(int fd, const void *buf, size_t count) {
if (fd != hprof_fd_) {
//如果是我们的hprof_fd_
return write(fd, buf, count);
}
// 每次hook_write,初始化重置
reset();
const unsigned char tag = ((unsigned char *) buf)[0];
// 删除掉无关record tag类型匹配,只匹配heap相关提高性能
switch (tag) {
// 我们关注的Record类型主要是HEAP DUMP,其中又分五个子类,
// 分别为GC ROOT、CLASS DUMP、INSTANCE DUMP、OBJECT ARRAY DUMP、PRIMITIVE ARRAY DUMP。
case HPROF_TAG_HEAP_DUMP:
case HPROF_TAG_HEAP_DUMP_SEGMENT: {
ProcessHeap(
buf,
HEAP_TAG_BYTE_SIZE + RECORD_TIME_BYTE_SIZE + RECORD_LENGTH_BYTE_SIZE,
count, heap_serial_num_, 0);
heap_serial_num_++;
}
break;
default:
break;
}
// 根据裁剪掉的zygote space和image space更新length
int record_length;
if (tag == HPROF_TAG_HEAP_DUMP || tag == HPROF_TAG_HEAP_DUMP_SEGMENT) {
record_length = GetIntFromBytes((unsigned char *) buf,
HEAP_TAG_BYTE_SIZE + RECORD_TIME_BYTE_SIZE);
record_length -= strip_bytes_sum_;//record_length删除了字节数
int index = HEAP_TAG_BYTE_SIZE + RECORD_TIME_BYTE_SIZE;
((unsigned char *) buf)[index] =//修改record_length
(unsigned char) (((unsigned int) record_length & 0xff000000u) >> 24u);
((unsigned char *) buf)[index + 1] =
(unsigned char) (((unsigned int) record_length & 0x00ff0000u) >> 16u);
((unsigned char *) buf)[index + 2] =
(unsigned char) (((unsigned int) record_length & 0x0000ff00u) >> 8u);
((unsigned char *) buf)[index + 3] =
(unsigned char) ((unsigned int) record_length & 0x000000ffu);
}
size_t total_write = 0;
int start_index = 0;
for (int i = 0; i < strip_index_; i++) {
// 将裁剪掉的区间,通过写时过滤掉
void *write_buf = (void *) ((unsigned char *) buf + start_index);
auto write_len = (size_t) (strip_index_list_pair_[i * 2] - start_index);
if (write_len > 0) {
total_write += write(fd, write_buf, write_len);
} else if (write_len < 0) {
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
"HookWrite array i:%d writeLen<0:%zu", i, write_len);
}
start_index = strip_index_list_pair_[i * 2 + 1];
}
auto write_len = (size_t) (count - start_index);
if (write_len > 0) {
void *write_buf = (void *) ((unsigned char *) buf + start_index);
total_write += write(fd, write_buf, count - start_index);
}
hook_write_serial_num_++;
if (total_write != count) {//说明裁剪发生了
__android_log_print(ANDROID_LOG_INFO, LOG_TAG,
"hook write, hprof strip happens");
}
return count;
}
void HprofStrip::HookInit() {
//使用xhook hook函数
xhook_enable_debug(0);
/**
*
* android 7.x,write方法在libc.so中
* android 8-9,write方法在libart.so中
* android 10,write方法在libartbase.so中
* libbase.so是一个保险操作,防止前面2个so里面都hook不到(:
*
* android 7-10版本,open方法都在libart.so中
* libbase.so与libartbase.so,为保险操作
*/
xhook_register("libart.so", "open", (void *) HookOpen, nullptr);
xhook_register("libbase.so", "open", (void *) HookOpen, nullptr);
xhook_register("libartbase.so", "open", (void *) HookOpen, nullptr);
xhook_register("libc.so", "write", (void *) HookWrite, nullptr);
xhook_register("libart.so", "write", (void *) HookWrite, nullptr);
xhook_register("libbase.so", "write", (void *) HookWrite, nullptr);
xhook_register("libartbase.so", "write", (void *) HookWrite, nullptr);
xhook_refresh(0);
xhook_clear();
}
//返回HprofStrip实例
HprofStrip &HprofStrip::GetInstance() {
static HprofStrip hprof_strip;
return hprof_strip;
}
//构造函数
HprofStrip::HprofStrip()
: hprof_fd_(-1),
strip_bytes_sum_(0),
heap_serial_num_(0),
hook_write_serial_num_(0),
strip_index_(0),
is_hook_success_(false),
is_current_system_heap_(false) {
//初始化strip_index_list_pair_
std::fill(strip_index_list_pair_,
strip_index_list_pair_ + arraysize(strip_index_list_pair_), 0);
}
//返回is_hook_success_
ALWAYS_INLINE bool HprofStrip::IsHookSuccess() const {
return is_hook_success_;
}
//设置hprof名字
void HprofStrip::SetHprofName(const char *hprof_name) {
hprof_name_ = hprof_name;
}
} // namespace leak_monitor
} // namespace kwai
四.hprof解析性能优化
Shark是如何优化的呢?
Shark是LeakCanary 2.0推出的全新解析组件,其设计思想详见作者的介绍[6],主要做了以下几项优化:
- 索引,shark低内存开销的最根本原因就是通过索引做到了内存懒加载,遍历hprof时存储对象在hprof中的位置,并为其建立索引方便按需解析。
- 数据结构上做了深度优化,主要是使用了更高效的map,有2个:第一是对于key和value都是基础类型或字符串的使用hppc做map,第二是对于value不是基本类型的,使用SortedBytesMap存储内容。
具体的索引有:实例索引、类索引、字符串索引、类名索引、数组索引:
/**
* This class is not thread safe, should be used from a single thread.
*/
internal class HprofInMemoryIndex private constructor(
private val positionSize: Int,
private val hprofStringCache: LongObjectScatterMap<String>,
private val classNames: LongLongScatterMap,
private val classIndex: SortedBytesMap,
private val instanceIndex: SortedBytesMap,
private val objectArrayIndex: SortedBytesMap,
private val primitiveArrayIndex: SortedBytesMap,
private val gcRoots: List<GcRoot>,
private val proguardMapping: ProguardMapping?,
val primitiveWrapperTypes: Set<Long>
) {
/**
* Code from com.carrotsearch.hppc.LongLongScatterMap copy pasted, inlined and converted to Kotlin.
*
* See https://github.com/carrotsearch/hppc .
*/
class LongLongScatterMap constructor(expectedElements: Int = 4) {
/**
* A read only map of `id` => `byte array` sorted by id, where `id` is a long if [longIdentifiers]
* is true and an int otherwise. Each entry has a value byte array of size [bytesPerValue].
*
* Instances are created by [UnsortedByteEntries]
*
* [get] and [contains] perform a binary search to locate a specific entry by key.
*/
internal class SortedBytesMap(
private val longIdentifiers: Boolean,
private val bytesPerValue: Int,
private val sortedEntries: ByteArray
) {
所谓hppc是High Performance Primitive Collection[7]的缩写,shark使用kotlin将其重写了。hppc只支持基本类型,所以没有了装、拆箱的性能损耗,相关集合操作也做了大量优化,其benchmark可以参考[8]。
经过一番探索与实践,中途还去研究了MAT的源码,我们对其主要做了以下几点优化:
- GC root剪枝,由于我们搜索Path to GC Root时,是从GC Root自顶向下BFS,如JavaFrame、MonitorUsed等此类GC Root可以直接剪枝。
- 基本类型、基本类型数组不搜索、不解析。
- 同类对象超过阈值时不再搜索。
- 增加预处理,缓存每个类的所有递归super class,减少重复计算。
- 将object ID的类型从long修改为int,Android虚拟机的object ID大小只有32位,目前shark里使用的都是long来存储的,OOM时百万级对象的情况下,可以节省10M内存。 =》 代码里没看出来
本文探讨了如何解决GC卡顿,通过内存阈值监控避免频繁触发GC。此外,文章详细介绍了如何解决Dump hprof导致的应用冻结问题,通过ForkJvmHeapDumper在子进程中异步执行,减少了用户感知的卡顿。同时,针对hprof文件过大的问题,提出了实时裁剪策略以优化磁盘占用。最后,提到了Shark解析性能的优化,包括索引、数据结构和GC根剪枝等策略。

被折叠的 条评论
为什么被折叠?



