仮想メモリも枯渇します
Android 開発者として、私たちは APP のアーキテクチャが 32 ビットから 64 ビットに切り替わったことを経験しているはずです。現時点では、国内市場では 32 ビット アーキテクチャの要件が依然として存在しており、包括的な禁止はありませんが、32 ビット アーキテクチャの欠点の 1 つは、ユーザー空間に割り当てられる仮想メモリが少なすぎることです (通常、半分は仮想メモリです)。カーネル領域用に予約されており、構成可能)、そのため、多くの場合、仮想メモリが発生し、メモリが不足すると OOM が発生します。64 ビット アーキテクチャに切り替えた後、ARM64 では、ページ サイズが 4kb の場合、プロセスに割り当てることができるデフォルトの仮想メモリ サイズは 2^39 です。実際には、64 ビットを完全に割り当てることはできませんが、39 ビットの大きさは利用可能な仮想メモリ サイズは 32 ビットのサイズよりもはるかに大きいため、多くの場合、64 ビット アーキテクチャにアップグレードすると、仮想メモリ不足の問題が軽減されます。ここでさらに興味深いのは、OOM は、アプリケーションが長期間存在する場合にのみ軽減されます。そうでない場合は、多数の仮想メモリ リークなど、仮想メモリが大きい場合でも、仮想メモリ不足が原因で OOM がトリガーされます。
例としては、ネイティブ スレッドの作成ではスタック領域の層を作成するために mmap が必要なため、mmap 割り当ての失敗が発生したり、メモリを割り当てるために mmap を呼び出す際のその他の失敗が挙げられます。
java.lang.OutOfMemoryError: Could not allocate JNI Env: Failed anonymous mmap(0x0, 8192, 0x34, 0x220, -1, 0): Out of memory. See process maps in the log.
at java.lang.Thread.nativeCreate(Thread.java)
at java.lang.Thread.start(Thread.java:733)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:975)
at java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1043)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1185)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:764)
実際、ART 仮想マシン自体が多くのネイティブ メモリ リークを引き起こすため、Android チームはネイティブ メモリ リークを検出するために Android N の後に libmemunreachable モジュールを追加しました。
libmemun到達可能
MemUnreachable.cppには、到達不能なメモリ アドレスを取得するメソッド GetUnreachableMemory が提供されています
GetUnreachableMemory(UnreachableMemoryInfo& info, size_t limit)
GetUnreachableMemory を介して、info 関数で到達不能なアドレスの情報を取得できます。
384 bytes in 9 allocations unreachable out of 20003960 bytes in 40784 allocations
384 bytes in 9 unreachable allocations
ABI: 'arm64'
320 bytes unreachable at 7e879d09c0
8 bytes unreachable at 7e8d891160
8 bytes unreachable at 7e8d9fec78
....
リークのサイズやリークのアドレスなど、まだ多くの情報が取得されていることがわかります。もちろん、そのほとんどは ART セルフテストに使用されます。たとえば、APP でネイティブ メモリ リークが発生した場合はどうすればよいかを監視したいと考えています。心配しないでください。方法はあります。使用する前に原理について説明しますが、興味がない場合は、直接使用セクションに移動してください。
考えてみましょう。メモリ リーク検出を行いたい場合、一般的にはどのように行うのでしょうか?
最初のステップは、メモリが到達可能かどうかを確認することですよね? このステップは非常に重要です。たとえば、Java ヒープ メモリはメモリが到達可能かどうかを確認します。実際、それはいくつかの gc ルートから始まります。参照が見つかったら既存のオブジェクトから gc ルートへのチェーンを実行すると、メモリが使用されていることがわかります。そうでない場合は、到達不可能なメモリとなり、仮想マシン GC によって再利用されます。
同じことがネイティブ層にも当てはまります。メモリに到達可能かどうかの検出も、ルート メモリに基づいて行われます (ルートとは、仮想マシンのヒープに関連付けられているメモリやスレッド内のメモリなど、現在使用されているメモリです)スタック範囲) を調べて、メモリ リークがあるかどうかを判断します。実際、ネイティブ メモリにルート参照チェーンのメモリがない限り、メモリ リークが発生しているはずなので、ネイティブの判断は Java 層の判断よりも簡単です。ネイティブ層には Java のような GC 機構がないため、リリースされていない場合は常に存在しており、これには注意が必要なので、リークしたメモリを探すと、到達できないメモリを見つけるだけになってしまいます。
ソースコードの解析に入ってみましょう
GetUnreachableMemory
bool GetUnreachableMemory(UnreachableMemoryInfo& info, size_t limit) {
if (info.version > 0) {
MEM_ALOGE("unsupported UnreachableMemoryInfo.version %zu in GetUnreachableMemory",
info.version);
return false;
}
int parent_pid = getpid();
int parent_tid = gettid();
Heap heap;
AtomicState<State> state(STARTING);
LeakPipe pipe;
PtracerThread thread{[&]() -> int {
/
// Collection thread
/
MEM_ALOGI("collecting thread info for process %d...", parent_pid);
if (!state.transition_or(STARTING, PAUSING, [&] {
MEM_ALOGI("collecting thread expected state STARTING, aborting");
return ABORT;
})) {
return 1;
}
ThreadCapture thread_capture(parent_pid, heap);
allocator::vector<ThreadInfo> thread_info(heap);
allocator::vector<Mapping> mappings(heap);
allocator::vector<uintptr_t> refs(heap);
这里主要做一些自检
// ptrace all the threads
if (!thread_capture.CaptureThreads()) {
state.set(ABORT);
return 1;
}
// collect register contents and stacks
if (!thread_capture.CapturedThreadInfo(thread_info)) {
state.set(ABORT);
return 1;
}
// snapshot /proc/pid/maps
if (!ProcessMappings(parent_pid, mappings)) {
state.set(ABORT);
return 1;
}
if (!BinderReferences(refs)) {
state.set(ABORT);
return 1;
}
// Atomically update the state from PAUSING to COLLECTING.
// The main thread may have given up waiting for this thread to finish
// pausing, in which case it will have changed the state to ABORT.
if (!state.transition_or(PAUSING, COLLECTING, [&] {
MEM_ALOGI("collecting thread aborting");
return ABORT;
})) {
return 1;
}
// malloc must be enabled to call fork, at_fork handlers take the same
// locks as ScopedDisableMalloc. All threads are paused in ptrace, so
// memory state is still consistent. Unfreeze the original thread so it
// can drop the malloc locks, it will block until the collection thread
// exits.
thread_capture.ReleaseThread(parent_tid);
因为存在耗时,所以fork子进程去处理检测
// fork a process to do the heap walking
int ret = fork();
if (ret < 0) {
return 1;
} else if (ret == 0) {
/
// Heap walker process
/
// Examine memory state in the child using the data collected above and
// the CoW snapshot of the process memory contents.
if (!pipe.OpenSender()) {
_exit(1);
}
MemUnreachable unreachable{parent_pid, heap};
这里很关键,是分析的开始,这里注意参数,是Root的起点
if (!unreachable.CollectAllocations(thread_info, mappings, refs)) {
_exit(2);
}
size_t num_allocations = unreachable.Allocations();
size_t allocation_bytes = unreachable.AllocationBytes();
allocator::vector<Leak> leaks{heap};
size_t num_leaks = 0;
size_t leak_bytes = 0;
前面配置好Root 后,就发起查找GetUnreachableMemory
bool ok = unreachable.GetUnreachableMemory(leaks, limit, &num_leaks, &leak_bytes);
检测完通过管道pipe通知到父进程即可
ok = ok && pipe.Sender().Send(num_allocations);
ok = ok && pipe.Sender().Send(allocation_bytes);
ok = ok && pipe.Sender().Send(num_leaks);
ok = ok && pipe.Sender().Send(leak_bytes);
ok = ok && pipe.Sender().SendVector(leaks);
if (!ok) {
_exit(3);
}
_exit(0);
} else {
// Nothing left to do in the collection thread, return immediately,
// releasing all the captured threads.
MEM_ALOGI("collection thread done");
return 0;
}
}};
/
// Original thread
/
{
// Disable malloc to get a consistent view of memory
ScopedDisableMalloc disable_malloc;
// Start the collection thread
thread.Start();
如果等待超时会abort
// Wait for the collection thread to signal that it is ready to fork the
// heap walker process.
if (!state.wait_for_either_of(COLLECTING, ABORT, 30s)) {
// The pausing didn't finish within 30 seconds, attempt to atomically
// update the state from PAUSING to ABORT. The collecting thread
// may have raced with the timeout and already updated the state to
// COLLECTING, in which case aborting is not necessary.
if (state.transition(PAUSING, ABORT)) {
MEM_ALOGI("main thread timed out waiting for collecting thread");
}
}
// Re-enable malloc so the collection thread can fork.
}
// Wait for the collection thread to exit
int ret = thread.Join();
if (ret != 0) {
return false;
}
// Get a pipe from the heap walker process. Transferring a new pipe fd
// ensures no other forked processes can have it open, so when the heap
// walker process dies the remote side of the pipe will close.
if (!pipe.OpenReceiver()) {
return false;
}
通过管道接受子进程处理好的数据,然后返回
bool ok = true;
ok = ok && pipe.Receiver().Receive(&info.num_allocations);
ok = ok && pipe.Receiver().Receive(&info.allocation_bytes);
ok = ok && pipe.Receiver().Receive(&info.num_leaks);
ok = ok && pipe.Receiver().Receive(&info.leak_bytes);
ok = ok && pipe.Receiver().ReceiveVector(info.leaks);
if (!ok) {
return false;
}
MEM_ALOGI("unreachable memory detection done");
MEM_ALOGE("%zu bytes in %zu allocation%s unreachable out of %zu bytes in %zu allocation%s",
info.leak_bytes, info.num_leaks, plural(info.num_leaks), info.allocation_bytes,
info.num_allocations, plural(info.num_allocations));
return true;
}
GetUnreachableMemory は実際にはエントリ メソッドです。fork 子プロセスを通じてメモリ リークを検出します。その理由は、現在のプロセスがメモリの割り当てを継続するためです。分析が必要な場合、プロセスはブロックされます。スレッドの一時停止などの操作が含まれるため、子プロセスを通過して分析します。サブプロセスが分析された後、パイプを通じてデータを書き戻すことができます。ここでは、CollectAllocations メソッドのマークに焦点を当てます。
ルートオブジェクト
CollectAllocations を導入する前に、Root オブジェクトがどのように追加されるかを知る必要があります。また、Root 参照チェーンから始めて、到達不能なメモリがリークされたメモリであることも先ほど述べたので、Root の選択は非常に重要です。ルートの追加方法は以下の通りです
void HeapWalker::Root(uintptr_t begin, uintptr_t end) {
roots_.push_back(Range{begin, end});
}
void HeapWalker::Root(const allocator::vector<uintptr_t>& vals) {
root_vals_.insert(root_vals_.end(), vals.begin(), vals.end());
}
ここで、CollectAllocations の次のステップは、Root オブジェクトを追加して検出をトリガーする必要があることがわかります。
割り当ての収集
bool MemUnreachable::CollectAllocations(const allocator::vector<ThreadInfo>& threads,
const allocator::vector<Mapping>& mappings,
const allocator::vector<uintptr_t>& refs) {
MEM_ALOGI("searching process %d for allocations", pid_);
for (auto it = mappings.begin(); it != mappings.end(); it++) {
heap_walker_.Mapping(it->begin, it->end);
}
同样做自检
allocator::vector<Mapping> heap_mappings{mappings};
allocator::vector<Mapping> anon_mappings{mappings};
allocator::vector<Mapping> globals_mappings{mappings};
allocator::vector<Mapping> stack_mappings{mappings};
if (!ClassifyMappings(mappings, heap_mappings, anon_mappings, globals_mappings, stack_mappings)) {
return false;
}
for (auto it = heap_mappings.begin(); it != heap_mappings.end(); it++) {
MEM_ALOGV("Heap mapping %" PRIxPTR "-%" PRIxPTR " %s", it->begin, it->end, it->name);
HeapIterate(*it,
[&](uintptr_t base, size_t size) { heap_walker_.Allocation(base, base + size); });
}
for (auto it = anon_mappings.begin(); it != anon_mappings.end(); it++) {
MEM_ALOGV("Anon mapping %" PRIxPTR "-%" PRIxPTR " %s", it->begin, it->end, it->name);
打上地址标记
heap_walker_.Allocation(it->begin, it->end);
}
for (auto it = globals_mappings.begin(); it != globals_mappings.end(); it++) {
MEM_ALOGV("Globals mapping %" PRIxPTR "-%" PRIxPTR " %s", it->begin, it->end, it->name);
设置map地址为root
heap_walker_.Root(it->begin, it->end);
}
for (auto thread_it = threads.begin(); thread_it != threads.end(); thread_it++) {
for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) {
if (thread_it->stack.first >= it->begin && thread_it->stack.first <= it->end) {
MEM_ALOGV("Stack %" PRIxPTR "-%" PRIxPTR " %s", thread_it->stack.first, it->end, it->name);
当前有效线程的栈地址 作为root
heap_walker_.Root(thread_it->stack.first, it->end);
}
}
heap_walker_.Root(thread_it->regs);
}
heap相关地址设置为root
heap_walker_.Root(refs);
MEM_ALOGI("searching done");
return true;
}
リークの検出
Root を構成した後、GetUnreachableMemory のサブプロセス処理ロジックに戻ります。このロジックには、次のようなコードが含まれます bool ok = unreachable.GetUnreachableMemory(leaks, limit, &num_leaks, &leak_bytes);
これは、リークの検出をトリガーするルートの構成です。
bool HeapWalker::DetectLeaks() {
// Recursively walk pointers from roots to mark referenced allocations
for (auto it = roots_.begin(); it != roots_.end(); it++) {
查找是否存在与Root的引用链
RecurseRoot(*it);
}
Range vals;
vals.begin = reinterpret_cast<uintptr_t>(root_vals_.data());
vals.end = vals.begin + root_vals_.size() * sizeof(uintptr_t);
RecurseRoot(vals);
if (segv_page_count_ > 0) {
MEM_ALOGE("%zu pages skipped due to segfaults", segv_page_count_);
}
return true;
}
アドレスとルートの接続を見つける
void HeapWalker::RecurseRoot(const Range& root) {
allocator::vector<Range> to_do(1, root, allocator_);
while (!to_do.empty()) {
Range range = to_do.back();
to_do.pop_back();
walking_range_ = range;
ForEachPtrInRange(range, [&](Range& ref_range, AllocationInfo* ref_info) {
if (!ref_info->referenced_from_root) {
如果能在有效地址找到,那么证明这个地址属于有效引用,标记为true
ref_info->referenced_from_root = true;
to_do.push_back(ref_range);
}
});
walking_range_ = Range{0, 0};
}
}
その後、リークアドレスを書き込む処理になりますが、これは前のソースコードで説明したので、ここでは詳しく説明しません。
libmemunreachable を使用する
これはシステム上のものですが、このメソッドを使用してリークされたメモリを取得することを妨げるものではなく、dlsym とシンボルを渡して GetUnreachableMemory メソッドを呼び出すだけで済みます。
GetUnreachableMemory シンボルは、Android のバージョンによって少し異なります。
API 26 を超えるシンボルは次のとおりです。
_ZN7android26GetUnreachableMemoryStringEbm
API 26 未満で 24 以上のシンボルは、
_Z26GetUnreachableMemoryStringbm
したがって、Android 7以降、dlopenには特定の制限があるため、シンボルを介して直接呼び出すことができます。ここでは、shadowhook_dlopenを直接使用して開くことができます(もちろん、組み込み関数の開始をシミュレートするなど、他の手段を使用することもできます) 、ここでは詳細には触れませんが、この記事の前半で説明しました)
void *handle = shadowhook_dlopen("libmemunreachable.so");
void *func;
if (android_get_device_api_level() > __ANDROID_API_O__) {
func = shadowhook_dlsym(handle,
"_ZN7android26GetUnreachableMemoryStringEbm");
} else {
func = shadowhook_dlsym(handle,
"_Z26GetUnreachableMemoryStringbm");
}
std::string result = ((std::string (*)(bool , size_t )) func)(false, 1024);
__android_log_print(ANDROID_LOG_ERROR, "hello", "%s", result.c_str());
return result;
もちろん、この関数を使用する前提として、解析データはptraceを使用しているため、prctlコールでDUMPABLEを1に設定する必要があるため、このフラグは必要です
if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) == -1) {
return unreachable_mem;
}
もちろん、取得するのは文字列の文字列であるため、内のサイズとアドレス情報だけが必要な場合は、正規表現を使用して有効な内容を抽出する必要もあります。内容は次のとおりです。
384 bytes in 9 allocations unreachable out of 20003960 bytes in 40784 allocations
384 bytes in 9 unreachable allocations
ABI: 'arm64'
320 bytes unreachable at 7e879d09c0
8 bytes unreachable at 7e8d891160
8 bytes unreachable at 7e8d9fec78
....
たとえば、必要なデータは 7e879d09c0 で到達できない 320 バイトだけであり、この行の 320 と 7e879d09c0 は次のコードで照合できます。
regex_t reg;
regmatch_t match[1];
匹配有效行
char *pattern = "[0-9]+ bytes unreachable at [A-Za-z0-9]+";
if (regcomp(®, pattern, REG_EXTENDED) != 0) {
printf("regcomp error\n");
return 1;
}
while (regexec(®, unreachable_memory, 1, match, 0) == 0) {
__android_log_print(ANDROID_LOG_ERROR, "hello",
"Match found at position %zd, length %ld: %.*s\n", match[0].rm_so,
match[0].rm_eo - match[0].rm_so, match[0].rm_eo - match[0].rm_so,
unreachable_memory + match[0].rm_so);
char result[100] = {""};
strncpy(result, unreachable_memory + match[0].rm_so, match[0].rm_eo - match[0].rm_so);
__android_log_print(ANDROID_LOG_ERROR, "hello", "裁剪字符串为 %s", result);
// 不关心字符串部分,只关心数字部分
unsigned long addr = strtoul(strrchr(result, ' ') + 1, NULL, 16);
unsigned long size = strtoul(result, NULL, 10);
__android_log_print(ANDROID_LOG_ERROR, "hello", "裁剪字符串size %lu %lu", size, addr);
unreachable_memory += match[0].rm_eo;
uint64_t leak = addr + size;
__android_log_print(ANDROID_LOG_ERROR, "hello", "leak is %lu", leak);
}
regfree(®);
要約する
この時点で、libmemunreachable を通じてリークしたメモリのアドレスとサイズを見つけることができます。もちろん、ここでの情報だけでは、リークしたスタック情報などを取得するのに十分ではない可能性があります。このとき、いくつかの割り当て関数をフックする必要があります。 malloc mmap など、ここでは説明しません。うーん、機会があればこの穴を埋めます。
やっと
アーキテクトになりたい場合、または 20,000 ~ 30,000 の給与範囲を突破したい場合は、コーディングとビジネスに限定されず、モデルを選択し、拡張し、プログラミング的思考を向上させることができなければなりません。また、しっかりとしたキャリアプランも大切で、学ぶ習慣も大切ですが、一番大切なのは継続力であり、継続的に実行できないプランは絵にかいたもちです。
方向性がわからない場合は、Ali のシニア アーキテクトが書いた一連の「Android の 8 つの主要モジュールに関する上級ノート」をここで共有したいと思います。これは、乱雑で散在し断片化した知識を体系的に整理するのに役立ちます。 Android開発のさまざまな知識を体系的かつ効率的に習得できます。
私たちが普段読んでいる断片的な内容と比べて、このノートの知識ポイントはより体系的で理解しやすく、覚えやすく、知識体系に従って厳密に配置されています。
ビデオ素材のフルセット:
1. インタビュー集
2. ソースコード解析集
3. オープンソース フレームワークのコレクションは、
ワン クリックと 3 つのリンクで誰でもサポートできるようになっています。記事内の情報が必要な場合は、記事の最後にある CSDN 公式認定 WeChat カードをクリックして無料で入手してください↓↓↓