ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

ART视角---如何在native内存增长过多时自动触发GC?如何在Java对象回收时触发native内存回收?

2021-11-23 23:00:31  阅读:351  来源: 互联网

标签:触发 bytes GC 内存 gc size native


接下来看看CheckGCForNative函数内部的逻辑。

首先计算当前native内存的总大小,然后计算当前内存大小和阈值之间的比值,如果比值≥1,则请求一次新的GC。

art/runtime/gc/heap.cc

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);
}
}
}

获取当前native内存的总大小需要调用GetNativeBytes函数。其内部统计也分为两部分,一部分是通过mallinfo获取的当前malloc的总大小。由于系统有专门的API获取这个信息,所以在NativeAllocationRegistry.registerNativeAllocation的时候不需要专门去存储单次malloc的大小。另一部分是native_bytes_registered_字段记录的所有注册过的mmap大小。二者相加,基本上反映了当前进程native内存的整体消耗。

art/runtime/gc/heap.cc

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.
}

得到当前进程native内存的总大小之后,便需要抉择是否需要一次新的GC。

决策的过程如下,源码下面是详细解释。

art/runtime/gc/heap.cc

// 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
// + c1* + 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(GetBytesAllocated() + weighted_native_bytes) <
    =(6)
    / static_cast(adj_start_bytes);
    }
    }

首先将本次native内存总大小和上一次GC完成后的native内存总大小进行比较。如果小于上次的总大小,则表明native内存的使用水平降低了,因此完全没有必要进行一次新的GC。

但如果这次native内存使用增长的话,则需要进一步计算当前值和阈值之间的比例关系,大于等于1的话就需要进行GC。下面详细介绍源码中的(1)~(6)。

(1)计算本次native内存和上次之间的差值,这个差值反映了native内存中新增长部分的大小。

(2)给不同部分的native内存以不同的权重,新增长部分除以2,旧的部分除以65536。之所以给旧的部分权重如此之低,是因为native堆本身是没有上限的。这套机制的初衷并不是限制native堆的大小,而只是防止两次GC间native内存垃圾积累过多。

(3)所谓的阈值并不是为native内存单独设立的,而是为(Java堆大小+native内存大小)整体设立的。add_bytes_allowed表示在原有Java堆阈值的基础上,还可以允许的native内存大小。NativeAllocationGcWatermark根据Java堆阈值计算出允许的native内存大小,Java堆阈值越大,允许的值也越大。HeapGrowthMultipiler对于前台应用是2,表明前台应用的内存管控更松,GC触发频率更低。

(4)同等条件下,同步GC的触发水位要低于非同步GC,原因是同步GC在垃圾回收时也会有新的对象分配,因此加上这些新分配的对象最好也不要超过阈值。

(5)将Java堆阈值和允许的native内存相加,作为新的阈值。

(6)将Java堆已分配的大小和调整权重后的native内存大小相加,并将相加后的结果除以阈值,得到一个比值来判定是否需要GC。

通过如下代码可知,当比值≥1时,将请求一次新的GC。

art/runtime/gc/heap.cc

if (UNLIKELY(gc_urgency >= 1.0)) {
if (is_gc_concurrent) {
RequestConcurrentGC(self, kGcCauseForNativeAlloc, /force_full=/true); <=================请求一次新的GC

1.2 MMap内存

mmap的处理方式和malloc基本相当,大于300,000 bytes或mmap三百次都执行CheckGCForNative。唯一的区别在于mmap需要将每一次的大小都计入native_bytes_registered中,因为mallinfo中并不会记录这个信息(针对bionic库而言)。

art/runtime/gc/heap.cc

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对象回收时触发native内存回收

NativeAllocationRegistry中主要依靠Cleaner机制完成了这个过程。关于Cleaner的细节,可以参考我的上篇博客

3. 实际案例

Bitmap类就是通过NativeAllocationRegistry来实现native资源自动释放的。以下是Bitmap构造方法的一部分。

frameworks/base/graphics/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);
}

标签:触发,bytes,GC,内存,gc,size,native
来源: https://blog.csdn.net/m0_64382868/article/details/121504809

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有