本文分析基于Android R(11)
序文
GCがJavaヒープメモリの回復に使用されることはよく知られている事実です。ただし、一部のJavaクラスはマリオネットとして設計されています。Javaオブジェクトは一部の「スレッド」のみを格納し、それらの実際のメモリ消費はすべてネイティブメモリに配置されます。たとえば、ビットマップ。彼らにとって、操作されたネイティブメモリを自動的に再利用する方法が緊急の問題になっています。
自動的にリサイクルするには、GCメカニズムに依存する必要があります。しかし、既存のGCメカニズムに依存するだけでは十分ではありません。また、次の2つの点を考慮する必要があります。
- 自動トリガーGC時のネイティブメモリの増加量
- GCJavaオブジェクトがネイティブリソースのリカバリを同期したときにリカバリする方法
AndroidはNからNativeAllocationRegistryクラスを導入しました。初期バージョンでは、GCがJavaオブジェクトを再利用するときにネイティブリソースが同期的に回復されるようにすることができ(上記のポイント2)、内部で使用するのは、前のブログで紹介したクリーナーメカニズムです。
以前のバージョンのNativeAllocationRegistryを使用すると、ネイティブリソースをリサイクルできますが、まだいくつかの欠陥があります。たとえば、マリオネットとして設計されたJavaクラスは小さなスペースを占有しますが、ネイティブリソースへの間接参照は多くを占めます。したがって、Javaヒープの成長は非常に遅く、ネイティブヒープの成長は非常に高速です。一部のシナリオでは、Javaヒープの成長が次のGCトリガーのレベルに達しておらず、ネイティブヒープ内のゴミが山に蓄積されています。プログラムによるプロアクティブな呼び出しSystem.gc()
は確かにこの問題を軽減することができますが、開発者はこの頻度をどのように制御しますか?頻繁に実行すると、実行パフォーマンスが低下します。まばらであると、ネイティブのガベージが時間内に解放されなくなります。そのため、新しいバージョンのNativeAllocationRegistryがGCと一緒に調整され、ネイティブメモリが大きくなりすぎたときにプロセスが自動的にGCをトリガーできるようになりました。これが上記の最初のポイントです。これは、Javaヒープの使用サイズのみを考慮する以前のGCトリガーと同等であり、現在はネイティブヒープと一緒に考慮されています。
ネイティブのゴミの蓄積の問題は、最近中国の多くの32ビットAPKで発生したネイティブメモリのOOM問題など、いくつかの深刻な問題を引き起こす可能性があります。ByteDanceは、その解決策を紹介するブログを特別に投稿しました。リンクされたブログでは、Bytedanceチームがアプリケーション層ソリューションを提供し、アプリケーション層がネイティブリソースを積極的にリリースします。しかし、この問題の根本的な解決策は、基礎となる設計の変更に依存しています。Bytedanceブログを読んだ後、私は特にAndroidチームに連絡し、CameraMetadataNativeクラスでNativeAllocationRegistryを使用することを提案しました。彼らはすぐに提案を受け入れ、新しい実装を提供しました。バイトビートによって発生するこの問題は、Sには存在しないと思います。
目次
1.ネイティブメモリが大きくなりすぎたときにGCを自動的にトリガーする方法
Javaクラスがマリオネットとして設計されている場合、通常、ネイティブメモリを割り当てる方法は2つあります。1つはヒープメモリを割り当てるmalloc(新しいものは通常mallocと呼ばれます)で、もう1つは匿名ページを割り当てるmmapです。2つの最大の違いは、mallocは通常、小さなメモリ割り当てに使用されるのに対し、mmapは通常大きなメモリ割り当てに使用されることです。
NativeAllocationRegistryを使用してこのJavaオブジェクトのネイティブメモリを自動的に解放する場合、最初にそれを呼び出す必要がありますregisterNativeAllocation
。一方で、今回はネイティブによって割り当てられたリソースサイズをGCに通知し、他方では、 GCトリガー条件に到達したかどうかを検出します。メモリの割り当て方法が異なると、処理方法も異なります。
libcore / luni / src / main / java / libcore / util / NativeAllocationRegistry.java
// Inform the garbage collector of the allocation. We do this differently for
// malloc-based allocations.
private static void registerNativeAllocation(long size) {
VMRuntime runtime = VMRuntime.getRuntime();
if ((size & IS_MALLOCED) != 0) { <==================如果native内存是通过malloc方式分配的,则走这个if分支
final long notifyImmediateThreshold = 300000;
if (size >= notifyImmediateThreshold) { <=========如果native内存大于等于300000bytes(~300KB),则走这个分支
runtime.notifyNativeAllocationsInternal();
} else { <==================如果native内存小于300000bytes,则走这个分支
runtime.notifyNativeAllocation();
}
} else {
runtime.registerNativeAllocation(size);
}
}
1.1Mallocメモリ
Mallocによって割り当てられたメモリには2つの判断条件があります。
- 割り当ては300,000バイト以上ですか。より大きい場合、
CheckGCForNative
関数はVIPチャネルを介して直接実行されます。この関数は、ネイティブメモリ割り当ての合計量をカウントし、GCトリガーのしきい値に達しているかどうかを判断します。到達すると、GCがトリガーされます。 - この割り当ては300の割り当ての整数倍ですか?この判定条件は
CheckGCForNative
、実行回数を制限するために使用され、300mallocごとに1つのテストのみが実行されます。
次に、CheckGCForNative
関数内のロジックを見てください。
最初に現在のネイティブメモリの合計サイズを計算してから、現在のメモリサイズとしきい値の比率を計算します。比率が1以上の場合は、新しいGCを要求します。
inline void Heap::CheckGCForNative(Thread* self) {
bool is_gc_concurrent = IsGcConcurrent();
size_t current_native_bytes = GetNativeBytes(); <================获取native内存的总大小
float gc_urgency = NativeMemoryOverTarget(current_native_bytes, is_gc_concurrent); <============计算当前内存大小和阈值之间的比值,大于等于1则表明需要一次新的GC
if (UNLIKELY(gc_urgency >= 1.0)) {
if (is_gc_concurrent) {
RequestConcurrentGC(self, kGcCauseForNativeAlloc, /*force_full=*/true); <=================请求一次新的GC
if (gc_urgency > kStopForNativeFactor
&& current_native_bytes > stop_for_native_allocs_) {
// We're in danger of running out of memory due to rampant native allocation.
if (VLOG_IS_ON(heap) || VLOG_IS_ON(startup)) {
LOG(INFO) << "Stopping for native allocation, urgency: " << gc_urgency;
}
WaitForGcToComplete(kGcCauseForNativeAlloc, self);
}
} else {
CollectGarbageInternal(NonStickyGcType(), kGcCauseForNativeAlloc, false);
}
}
}
現在のネイティブメモリの合計サイズを取得するには、GetNativeBytes
関数を呼び出す必要があります。その内部統計も2つの部分に分けられます。1つの部分はmallinfo
、を通じて取得された現在のmallocの合計サイズです。システムにはこの情報を取得するための特別なAPIがあるためNativeAllocationRegistry.registerNativeAllocation
、単一のmallocのサイズを格納する必要はありません。他の部分は、native_bytes_registered_フィールドに記録されたすべての登録済みmmapのサイズです。2つの追加は、基本的に、現在のプロセスでのネイティブメモリの全体的な消費を反映しています。
size_t Heap::GetNativeBytes() {
size_t malloc_bytes;
#if defined(__BIONIC__) || defined(__GLIBC__)
IF_GLIBC(size_t mmapped_bytes;)
struct mallinfo mi = mallinfo();
// In spite of the documentation, the jemalloc version of this call seems to do what we want,
// and it is thread-safe.
if (sizeof(size_t) > sizeof(mi.uordblks) && sizeof(size_t) > sizeof(mi.hblkhd)) {
// Shouldn't happen, but glibc declares uordblks as int.
// Avoiding sign extension gets us correct behavior for another 2 GB.
malloc_bytes = (unsigned int)mi.uordblks;
IF_GLIBC(mmapped_bytes = (unsigned int)mi.hblkhd;)
} else {
malloc_bytes = mi.uordblks;
IF_GLIBC(mmapped_bytes = mi.hblkhd;)
}
// From the spec, it appeared mmapped_bytes <= malloc_bytes. Reality was sometimes
// dramatically different. (b/119580449 was an early bug.) If so, we try to fudge it.
// However, malloc implementations seem to interpret hblkhd differently, namely as
// mapped blocks backing the entire heap (e.g. jemalloc) vs. large objects directly
// allocated via mmap (e.g. glibc). Thus we now only do this for glibc, where it
// previously helped, and which appears to use a reading of the spec compatible
// with our adjustment.
#if defined(__GLIBC__)
if (mmapped_bytes > malloc_bytes) {
malloc_bytes = mmapped_bytes;
}
#endif // GLIBC
#else // Neither Bionic nor Glibc
// We should hit this case only in contexts in which GC triggering is not critical. Effectively
// disable GC triggering based on malloc().
malloc_bytes = 1000;
#endif
return malloc_bytes + native_bytes_registered_.load(std::memory_order_relaxed);
// An alternative would be to get RSS from /proc/self/statm. Empirically, that's no
// more expensive, and it would allow us to count memory allocated by means other than malloc.
// However it would change as pages are unmapped and remapped due to memory pressure, among
// other things. It seems risky to trigger GCs as a result of such changes.
}
現在のプロセスのネイティブメモリの合計サイズを取得したら、新しいGCが必要かどうかを判断する必要があります。
意思決定プロセスは次のとおりです。ソースコードについては、以下で詳しく説明します。
// Return the ratio of the weighted native + java allocated bytes to its target value.
// A return value > 1.0 means we should collect. Significantly larger values mean we're falling
// behind.
inline float Heap::NativeMemoryOverTarget(size_t current_native_bytes, bool is_gc_concurrent) {
// Collection check for native allocation. Does not enforce Java heap bounds.
// With adj_start_bytes defined below, effectively checks
// <java bytes allocd> + c1*<old native allocd> + c2*<new native allocd) >= adj_start_bytes,
// where c3 > 1, and currently c1 and c2 are 1 divided by the values defined above.
size_t old_native_bytes = old_native_bytes_allocated_.load(std::memory_order_relaxed);
if (old_native_bytes > current_native_bytes) {
// Net decrease; skip the check, but update old value.
// It's OK to lose an update if two stores race.
old_native_bytes_allocated_.store(current_native_bytes, std::memory_order_relaxed);
return 0.0;
} else {
size_t new_native_bytes = UnsignedDifference(current_native_bytes, old_native_bytes); <=======(1)
size_t weighted_native_bytes = new_native_bytes / kNewNativeDiscountFactor <=======(2)
+ old_native_bytes / kOldNativeDiscountFactor;
size_t add_bytes_allowed = static_cast<size_t>( <=======(3)
NativeAllocationGcWatermark() * HeapGrowthMultiplier());
size_t java_gc_start_bytes = is_gc_concurrent <=======(4)
? concurrent_start_bytes_
: target_footprint_.load(std::memory_order_relaxed);
size_t adj_start_bytes = UnsignedSum(java_gc_start_bytes, <=======(5)
add_bytes_allowed / kNewNativeDiscountFactor);
return static_cast<float>(GetBytesAllocated() + weighted_native_bytes) <=======(6)
/ static_cast<float>(adj_start_bytes);
}
}
まず、今回のネイティブメモリの合計サイズを最後のGC後のネイティブメモリの合計サイズと比較します。前回の合計サイズよりも小さい場合は、ネイティブメモリの使用量が減少していることを示しているため、新たにGCを実行する必要はありません。
ただし、今回ネイティブメモリ使用量が増加した場合は、現在値としきい値の比例関係をさらに計算する必要があります。1以上の場合はGCが必要です。ソースコードの以下の詳細(1)〜(6)。
(1)このネイティブメモリと最後のメモリの差を計算します。この差は、ネイティブメモリの新しく増加した部分のサイズを反映しています。
(2)ネイティブメモリのさまざまな部分にさまざまな重みを与え、新しい成長部分を2で割り、古い部分を65536で割ります。古い部分の重量が非常に軽いのは、ネイティブヒープ自体に上限がないためです。このメカニズムの本来の目的は、ネイティブヒープのサイズを制限することではなく、2つのGC間でネイティブメモリのガベージが過度に蓄積されるのを防ぐことです。
(3)いわゆるしきい値は、ネイティブメモリだけではなく、(Javaヒープサイズ+ネイティブメモリサイズ)全体に対して設定されます。add_bytes_allowedは、元のJavaヒープしきい値に基づいて許可できるネイティブメモリサイズを表します。NativeAllocationGcWatermark
許容されるネイティブメモリサイズは、Javaヒープのしきい値に従って計算されます。Javaヒープのしきい値が大きいほど、許容値は大きくなります。HeapGrowthMultipiler
フォアグラウンドアプリケーションの場合は2です。これは、フォアグラウンドアプリケーションのメモリ制御が緩く、GCトリガー周波数が低いことを示しています。
(4)同じ条件で、同期GCのトリガーレベルは非同期GCのトリガーレベルよりも低くなります。その理由は、同期GCでもガベージコレクション中に新しいオブジェクトが割り当てられるため、しきい値を超えないようにすることをお勧めします。これらの新しく割り当てられたオブジェクトに対して。
(5)Javaヒープしきい値と許可されたネイティブメモリを新しいしきい値として追加します。
(6)重みを調整した後、Javaヒープの割り当てサイズとネイティブメモリサイズを加算し、加算した結果をしきい値で除算して、GCが必要かどうかを判断する比率を求めます。
次のコードは、比率が1以上の場合、新しいGCが要求されることを示しています。
if (UNLIKELY(gc_urgency >= 1.0)) {
if (is_gc_concurrent) {
RequestConcurrentGC(self, kGcCauseForNativeAlloc, /*force_full=*/true); <=================请求一次新的GC
1.2MMapメモリ
mmapの処理方法は、基本的には、300,000バイトまたはmmapを300回実行するmallocの処理方法と同じCheckGCForNative
です。唯一の違いは、この情報が(bionicライブラリの)mallinfoに記録されないため、mmapは毎回のサイズをnative_bytes_registeredにカウントする必要があることです。
void Heap::RegisterNativeAllocation(JNIEnv* env, size_t bytes) {
// Cautiously check for a wrapped negative bytes argument.
DCHECK(sizeof(size_t) < 8 || bytes < (std::numeric_limits<size_t>::max() / 2));
native_bytes_registered_.fetch_add(bytes, std::memory_order_relaxed);
uint32_t objects_notified =
native_objects_notified_.fetch_add(1, std::memory_order_relaxed);
if (objects_notified % kNotifyNativeInterval == kNotifyNativeInterval - 1
|| bytes > kCheckImmediatelyThreshold) {
CheckGCForNative(ThreadForEnv(env));
}
}
2.Javaオブジェクトがリサイクルされるときにネイティブメモリのリサイクルをトリガーする方法
NativeAllocationRegistryは、主にCleanerメカニズムに依存してこのプロセスを完了します。Cleanerの詳細については、以前のブログを参照してください。
3.実際のケース
ビットマップクラスは、NativeAllocationRegistryを介したネイティブリソースの自動リリースを実装します。以下は、ビットマップ構築方法の一部です。
フレームワーク/ベース/グラフィックス/java/android/graphics/Bitmap.java
mNativePtr = nativeBitmap; <=========================== 通过指针值间接持有native资源
final int allocationByteCount = getAllocationByteCount(); <==== 获取native资源的大小,如果是mmap方式,这个大小最终会计入native_bytes_registered中
NativeAllocationRegistry registry;
if (fromMalloc) {
registry = NativeAllocationRegistry.createMalloced( <==== 根据native资源分配方式的不同,构造不同的NativeAllocationRegistry对象,nativeGetNativeFinalizer()返回的是native资源释放函数的函数指针
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
} else {
registry = NativeAllocationRegistry.createNonmalloced(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
}
registry.registerNativeAllocation(this, nativeBitmap); <===== 检测是否需要GC
上記のケースから、NativeAllocationRegistryを使用してJavaクラスのネイティブメモリリソースを自動的に解放する場合、最初にNativeAllocationRegistryオブジェクトを作成してから、registerNativeAllocation
メソッドを呼び出す必要があることがわかります。これらの2つのステップだけが、ネイティブリソースの自動解放を実現できます。
2つのステップが必要なのでregisterNativeAllocation
、NativeAllocationRegistryの構築メソッドに入れてみませんか?この1つのステップを実行する方が良いのではないでしょうか。その理由はregisterNativeAllocation
、独立している場合は、ネイティブリソースが実際に適用された後で、GCに通知できるためです。これはより柔軟です。さらに、NativeAllocationRegistryにはregisterNativeFree
対応するメソッドがあり、ネイティブリソースを事前に解放した後、アプリケーション層がGCに通知できるようにします。
著者:Lu Mid
リンク:https://juejin.im/post/6894153239907237902
文末
私をフォローし、Androidの乾物を共有し、Androidのテクノロジーを交換してくれてありがとう。
記事に関する洞察や技術的な質問がある場合は、コメント領域にメッセージを残して話し合うことができます。私はあなたに誠実に答えます。
誰もが私のBステーションに遊びに来てくれます。昇進して給料を上げるのに役立つ、さまざまなAndroidアーキテクトの高度な技術的困難についてのビデオ説明があります。
駅Bの電車経由:https://space.bilibili.com/544650554