ART 虚拟机 | 如何让 GC 同步回收 native 内存

发表于 2年以前  | 总阅读数:336 次

本文分析基于Android R(11)

前言

GC用于Java堆内存的回收,这是人尽皆知的事实。然而现在有些Java类被设计成牵线木偶,Java对象只存储一些“线”,其真实的内存消耗全都放到了native内存中。譬如Bitmap。对它们而言,如何自动回收操纵的native内存成为一个亟须解决的问题。

想要自动回收,必须依赖GC机制。但仅仅依靠现有的GC机制还不够。我们还需要考虑以下两点:

  1. 如何在native内存增长过多的时候 自动 触发GC
  2. 如何在GC回收Java对象时 同步回收 native资源

Android从N开始引入了NativeAllocationRegistry类。早期的版本可以保证在GC回收Java对象时同步回收native资源(上述第2点),其内部用到的正是上一篇博客介绍过的Cleaner机制。

利用早期版本的NativeAllocationRegistry,native资源虽然可以回收,但仍然有些缺陷。譬如被设计成牵线木偶的Java类所占空间很小,但其间接引用的native资源占用很大。因此就会导致Java堆的增长很慢,而native堆的增长很快。

在某些场景下,Java堆的增长还没有达到下一次GC触发的水位,而native堆中的垃圾已经堆积成山。由程序主动调用System.gc()当然可以缓解这个问题,但开发者如何控制这个频率?频繁的话就会降低运行性能,稀疏的话就会导致native垃圾无法及时释放。

因此新版本的NativeAllocationRegistry连同GC一起做了调整,使得进程在native内存增长过多的时候可以自动触发GC,也即上述的第1点。相当于以前的GC触发只考虑Java堆的使用大小,现在连同native堆一起考虑进去了。

native垃圾堆积成山的问题会导致一些严重的问题,譬如最近国内很多32位APK上碰到过的native内存OOM问题,其中字节跳动专门发过博客介绍他们的解决方案。在链接的博客里,字节跳动团队提供了应用层的解决方案,由应用层来主动释放native资源。

但这个问题的根本解决还得依赖底层设计的修改。看完字节跳动的博客后,我专门联系过Android团队,建议他们在CameraMetadataNative类中使用NativeAllocationRegistry。他们很快接受了这个提议,并提供了新的实现。相信字节跳动遇到的这个问题在S上将不会存在。

目录

1 . 如何在native内存增长过多时自动触发GC

当Java类被设计成牵线木偶时,其native内存的分配通常有两种方式。一种是malloc(new的内部通常也是调用malloc)分配堆内存,另一种是mmap分配匿名页。二者最大的区别是malloc通常用于小内存分配,而mmap通常用于大内存分配。

当我们使用NativeAllocationRegistry为该Java对象自动释放native内存时,首先需要调用registerNativeAllocation,一方面告知GC本次native分配的资源大小,另一方面检测是否达到GC的触发条件。根据内存分配方式的不同,处理方式也不太一样。

libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java

290     // Inform the garbage collector of the allocation. We do this differently for
291     // malloc-based allocations.
292     private static void registerNativeAllocation(long size) {
293         VMRuntime runtime = VMRuntime.getRuntime();
294         if ((size & IS_MALLOCED) != 0) {     <==================如果native内存是通过malloc方式分配的,则走这个if分支
295             final long notifyImmediateThreshold = 300000;
296             if (size >= notifyImmediateThreshold) {   <=========如果native内存大于等于300000bytes(~300KB),则走这个分支
297                 runtime.notifyNativeAllocationsInternal();
298             } else {                         <==================如果native内存小于300000bytes,则走这个分支
299                 runtime.notifyNativeAllocation();
300             }
301         } else {
302             runtime.registerNativeAllocation(size);
303         }
304     }

1.1 Malloc内存

Malloc分配的内存会有两个判断条件。

  1. 此次分配是否大于等于300,000bytes。大于的话则走VIP通道直接执行CheckGCForNative函数。该函数内部会统计native内存分配的总量,判断其是否达到GC触发的阈值。如果达到的话则触发一次GC。
  2. 此次分配是否是300次分配的整数倍。这个判定条件用于限定CheckGCForNative的执行次数,每300次malloc才去执行一次检测。

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

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

art/runtime/gc/heap.cc

3927 inline void Heap::CheckGCForNative(Thread* self) {
3928   bool is_gc_concurrent = IsGcConcurrent();
3929   size_t current_native_bytes = GetNativeBytes();    <================获取native内存的总大小
3930   float gc_urgency = NativeMemoryOverTarget(current_native_bytes, is_gc_concurrent); <============计算当前内存大小和阈值之间的比值,大于等于1则表明需要一次新的GC
3931   if (UNLIKELY(gc_urgency >= 1.0)) {
3932     if (is_gc_concurrent) {
3933       RequestConcurrentGC(self, kGcCauseForNativeAlloc, /*force_full=*/true);   <=================请求一次新的GC
3934       if (gc_urgency > kStopForNativeFactor
3935           && current_native_bytes > stop_for_native_allocs_) {
3936         // We're in danger of running out of memory due to rampant native allocation.
3937         if (VLOG_IS_ON(heap) || VLOG_IS_ON(startup)) {
3938           LOG(INFO) << "Stopping for native allocation, urgency: " << gc_urgency;
3939         }
3940         WaitForGcToComplete(kGcCauseForNativeAlloc, self);
3941       }
3942     } else {
3943       CollectGarbageInternal(NonStickyGcType(), kGcCauseForNativeAlloc, false);
3944     }
3945   }
3946 }

获取当前native内存的总大小需要调用GetNativeBytes函数。其内部统计也分为两部分,一部分是通过mallinfo获取的当前malloc的总大小。

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

art/runtime/gc/heap.cc

2533 size_t Heap::GetNativeBytes() {
2534   size_t malloc_bytes;
2535 #if defined(__BIONIC__) || defined(__GLIBC__)
2536   IF_GLIBC(size_t mmapped_bytes;)
2537   struct mallinfo mi = mallinfo();
2538   // In spite of the documentation, the jemalloc version of this call seems to do what we want,
2539   // and it is thread-safe.
2540   if (sizeof(size_t) > sizeof(mi.uordblks) && sizeof(size_t) > sizeof(mi.hblkhd)) {
2541     // Shouldn't happen, but glibc declares uordblks as int.
2542     // Avoiding sign extension gets us correct behavior for another 2 GB.
2543     malloc_bytes = (unsigned int)mi.uordblks;
2544     IF_GLIBC(mmapped_bytes = (unsigned int)mi.hblkhd;)
2545   } else {
2546     malloc_bytes = mi.uordblks;
2547     IF_GLIBC(mmapped_bytes = mi.hblkhd;)
2548   }
2549   // From the spec, it appeared mmapped_bytes <= malloc_bytes. Reality was sometimes
2550   // dramatically different. (b/119580449 was an early bug.) If so, we try to fudge it.
2551   // However, malloc implementations seem to interpret hblkhd differently, namely as
2552   // mapped blocks backing the entire heap (e.g. jemalloc) vs. large objects directly
2553   // allocated via mmap (e.g. glibc). Thus we now only do this for glibc, where it
2554   // previously helped, and which appears to use a reading of the spec compatible
2555   // with our adjustment.
2556 #if defined(__GLIBC__)
2557   if (mmapped_bytes > malloc_bytes) {
2558     malloc_bytes = mmapped_bytes;
2559   }
2560 #endif  // GLIBC
2561 #else  // Neither Bionic nor Glibc
2562   // We should hit this case only in contexts in which GC triggering is not critical. Effectively
2563   // disable GC triggering based on malloc().
2564   malloc_bytes = 1000;
2565 #endif
2566   return malloc_bytes + native_bytes_registered_.load(std::memory_order_relaxed);
2567   // An alternative would be to get RSS from /proc/self/statm. Empirically, that's no
2568   // more expensive, and it would allow us to count memory allocated by means other than malloc.
2569   // However it would change as pages are unmapped and remapped due to memory pressure, among
2570   // other things. It seems risky to trigger GCs as a result of such changes.
2571 }

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

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

art/runtime/gc/heap.cc

3897 // Return the ratio of the weighted native + java allocated bytes to its target value.
3898 // A return value > 1.0 means we should collect. Significantly larger values mean we're falling
3899 // behind.
3900 inline float Heap::NativeMemoryOverTarget(size_t current_native_bytes, bool is_gc_concurrent) {
3901   // Collection check for native allocation. Does not enforce Java heap bounds.
3902   // With adj_start_bytes defined below, effectively checks
3903   // <java bytes allocd> + c1*<old native allocd> + c2*<new native allocd) >= adj_start_bytes,
3904   // where c3 > 1, and currently c1 and c2 are 1 divided by the values defined above.
3905   size_t old_native_bytes = old_native_bytes_allocated_.load(std::memory_order_relaxed);
3906   if (old_native_bytes > current_native_bytes) {
3907     // Net decrease; skip the check, but update old value.
3908     // It's OK to lose an update if two stores race.
3909     old_native_bytes_allocated_.store(current_native_bytes, std::memory_order_relaxed);
3910     return 0.0;
3911   } else {
3912     size_t new_native_bytes = UnsignedDifference(current_native_bytes, old_native_bytes);   <=======(1)
3913     size_t weighted_native_bytes = new_native_bytes / kNewNativeDiscountFactor              <=======(2)
3914         + old_native_bytes / kOldNativeDiscountFactor;
3915     size_t add_bytes_allowed = static_cast<size_t>(                                         <=======(3)
3916         NativeAllocationGcWatermark() * HeapGrowthMultiplier());
3917     size_t java_gc_start_bytes = is_gc_concurrent                                           <=======(4)
3918         ? concurrent_start_bytes_
3919         : target_footprint_.load(std::memory_order_relaxed);
3920     size_t adj_start_bytes = UnsignedSum(java_gc_start_bytes,                               <=======(5)
3921                                          add_bytes_allowed / kNewNativeDiscountFactor);
3922     return static_cast<float>(GetBytesAllocated() + weighted_native_bytes)                  <=======(6)
3923          / static_cast<float>(adj_start_bytes);
3924   }
3925 }

首先将本次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

3931   if (UNLIKELY(gc_urgency >= 1.0)) {
3932     if (is_gc_concurrent) {
3933       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

3957 void Heap::RegisterNativeAllocation(JNIEnv* env, size_t bytes) {
3958   // Cautiously check for a wrapped negative bytes argument.
3959   DCHECK(sizeof(size_t) < 8 || bytes < (std::numeric_limits<size_t>::max() / 2));
3960   native_bytes_registered_.fetch_add(bytes, std::memory_order_relaxed);
3961   uint32_t objects_notified =
3962       native_objects_notified_.fetch_add(1, std::memory_order_relaxed);
3963   if (objects_notified % kNotifyNativeInterval == kNotifyNativeInterval - 1
3964       || bytes > kCheckImmediatelyThreshold) {
3965     CheckGCForNative(ThreadForEnv(env));
3966   }
3967 }

2 . 如何在Java对象回收时触发native内存回收

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

3 . 实际案例

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

frameworks/base/graphics/java/android/graphics/Bitmap.java

155         mNativePtr = nativeBitmap;         <=========================== 通过指针值间接持有native资源
156 
157         final int allocationByteCount = getAllocationByteCount(); <==== 获取native资源的大小,如果是mmap方式,这个大小最终会计入native_bytes_registered中
158         NativeAllocationRegistry registry;
159         if (fromMalloc) {
160             registry = NativeAllocationRegistry.createMalloced(   <==== 根据native资源分配方式的不同,构造不同的NativeAllocationRegistry对象,nativeGetNativeFinalizer()返回的是native资源释放函数的函数指针
161                     Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
162         } else {
163             registry = NativeAllocationRegistry.createNonmalloced(
164                     Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
165         }
166         registry.registerNativeAllocation(this, nativeBitmap);   <===== 检测是否需要GC

通过上述案例可知,当我们使用NativeAllocationRegistry来为Java类自动释放native内存资源时,首先需要创建NativeAllocationRegistry对象,接着调用registerNativeAllocation方法。只此两步,便可实现native资源的自动释放。

既然需要两步,那为什么registerNativeAllocation不放进NativeAllocationRegistry的构造方法,这样一步岂不是更好?

原因是registerNativeAllocation独立出来,便可以在native资源真正申请后再去告知GC,灵活性更高。此外,NativeAllocationRegistry中还有一个registerNativeFree方法与之对应,可以让应用层在自己提前释放native资源后告知GC。

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/z8yZzsJnzNv8dCFVJkkGJg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237227次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录