专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
郭霖  ·  StateFlow 和 ... ·  1 周前  
郭霖  ·  Android ... ·  6 天前  
鸿洋  ·  一个大型 Android 项目的模块划分哲学 ·  4 天前  
鸿洋  ·  细嗅蔷薇,Gradle 系列之 Task 必知必会 ·  5 天前  
51好读  ›  专栏  ›  郭霖

Android Native内存越用越多,会不会触发GC?

郭霖  · 公众号  · android  · 2024-11-18 08:00

正文



/   今日科技快讯   /


近日,我国自主设计建造的首艘大洋钻探船“梦想”号在广州正式入列,标志着我国深海探测关键技术装备取得重大突破。“梦想”号钻采系统国际领先,最大钻深可达11000米,具备4种钻探模式和3种取心方式,有望助力全球科学家实现“打穿地壳、进入地球深部”的科学梦想,为我国深海资源勘探、关键技术装备研发以及全球科学家开展大洋科学钻探研究提供重大平台支撑。

/   作者简介   /


大家周一好,新的一周我们继续加油!


本篇文章转自Pika的博客,文章主要分享了如何使用 Android 开发中 GC 相关的内容,相信会对大家有所帮助!


原文地址:

https://juejin.cn/post/7432327923213205555


/   前言   /


最近都在更新鸿蒙相关的话题,android 的文章比较少,我们来聊一个Android 中有趣的话题,还是 GC。


/   一个有趣的话题   /


我们都知道 Java 虚拟机中都会有垃圾回收机制(GC),有了垃圾回收机制的存在,虚拟机可以根据策略去回收一些被释放的 Java 对象,从而保证整个内存的空间不至于无限增长,一般的策略就是看 Java 虚拟机中的内存空间大小占比去决定要不要进行 GC,那么问题来了,Native 中分配的内存,会不会影响虚拟机的 GC 回收策略呢?换句话来说,Native 内存越多,会不会触发 GC?


答案是会的,ART 虚拟机中有这种机制,不过可以说是“间接的”。


/   ART中关于Native内存占用导致的GC   /


在 ART GC 策略中,触发常规 GC 时,GC大致触发可以分为策略触发 GC RequestConcurrentGC 与分配时 GC(AllocateInternalWithGc),前者通常是内存总数达到一定策略时触发,后者是内存不足时进行的 GC 触发,GC 的原因以 GcCause 这个枚举类给出,而我们今天的话题就是,GcCause 为 kGcCauseForNativeAlloc 时因为 Native 内存分配引起的 GC。


enum GcCause {
    ...
    // GC triggered for a native allocation when NativeAllocationGcWatermark is exceeded.
    // (This may be a blocking GC depending on whether we run a non-concurrent collector).
    kGcCauseForNativeAlloc,

那么什么时候会触发 GcCause 为 kGcCauseForNativeAlloc 类型的回收呢,其实它的链路非常简单,通过 CheckGCForNative 函数触发,GC 的策略依据是否可以并发有些小区别(is_gc_concurrent 是否为 true),关键在于这个变量 gc_urgency 是否大于等于1,如果满足条件则才会进行 gc 处理。


inline void Heap::CheckGCForNative(Thread* self) {
  bool is_gc_concurrent = IsGcConcurrent();
  uint32_t starting_gc_num = GetCurrentGcNum();
  size_t current_native_bytes = GetNativeBytes();
  float gc_urgency = NativeMemoryOverTarget(current_native_bytes, is_gc_concurrent);
  // 关键在于gc_curgency 这个系数是否大于等于1,小于则不触发gc
  if (UNLIKELY(gc_urgency >= 1.0)) {
     根据是否支持并行gc有小区别
    if (is_gc_concurrent) {
      bool requested =
          RequestConcurrentGC(self, kGcCauseForNativeAlloc, /*force_full=*/true, starting_gc_num);
      if (requested && 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) <        }
        // Count how many times we do this, so we can warn if this becomes excessive.
        // Stop after a while, out of excessive caution.
        static constexpr int kGcWaitIters = 20;
        for (int i = 1; i <= kGcWaitIters; ++i) {
          if (!GCNumberLt(GetCurrentGcNum(), max_gc_requested_.load(std::memory_order_relaxed))
              || WaitForGcToComplete(kGcCauseForNativeAlloc, self) != collector::kGcTypeNone) {
            break;
          }
          CHECK(GCNumberLt(starting_gc_num, max_gc_requested_.load(std::memory_order_relaxed)));
          if (i % 10 == 0) {
            LOG(WARNING) <          }
          static constexpr int kGcWaitSleepMicros = 2000;
          usleep(kGcWaitSleepMicros);  // Encourage our requested GC to start.
        }
      }
    } else {
      CollectGarbageInternal(NonStickyGcType(), kGcCauseForNativeAlloc, false, starting_gc_num + 1);
    }
  }
}

gc_urgency 这个变量其实就是当前的权重,因为触发 GC 是有成本的,过度使用并不好,我们可以看到这个系数通过 NativeMemoryOverTarget 方法计算,参数是 GetNativeBytes() 得到的变量,即当前 Native 占用内存大小。


inline float Heap::NativeMemoryOverTarget(size_t current_native_bytes, bool is_gc_concurrent) {
  判断上一次触发CheckGCForNative 的内存大小,如果不超过上一次,那么不触发,这是一个优化策略,即然上次都能够分配成功了,那么也就不必要再触发一次gc
  size_t old_native_bytes = old_native_bytes_allocated_.load(std::memory_order_relaxed);
  if (old_native_bytes > current_native_bytes) {
    记录本次数值,结束
    old_native_bytes_allocated_.store(current_native_bytes, std::memory_order_relaxed);
    return 0.0;
  } else {
    否则通过当前已经使用的native内存与上一次记录的内存进行计算
    size_t new_native_bytes = UnsignedDifference(current_native_bytes, old_native_bytes);
    size_t weighted_native_bytes = new_native_bytes / kNewNativeDiscountFactor
        + old_native_bytes / kOldNativeDiscountFactor;
    size_t add_bytes_allowed = static_cast(
        NativeAllocationGcWatermark() * HeapGrowthMultiplier());
    size_t java_gc_start_bytes = is_gc_concurrent
        ? concurrent_start_bytes_
        : target_footprint_.load(std::memory_order_relaxed);
    size_t adj_start_bytes = UnsignedSum(java_gc_start_bytes,
                                         add_bytes_allowed / kNewNativeDiscountFactor);
                                         
    最终的结果是通过native内存的比值与java内存的系数做运算得出最终的比值
    return static_cast<float>(GetBytesAllocated() + weighted_native_bytes)
         / static_cast<float>(adj_start_bytes);
  }
}

NativeMemoryOverTarget 函数通过 Native 内存与上一次记录的内存进行了加权计算,和最终 Java 内存使用得到最终的数值,这其实是一个内存调控算法,目的就是为了在尽力减少 GC 的情况下,保证最大效率触发 GC 我们再来学习一下 ART 虚拟机是如何统计 Native 内存的。


size_t Heap::GetNativeBytes() {
  size_t malloc_bytes;
#if defined(__BIONIC__) || defined(__GLIBC__) || defined(ANDROID_HOST_MUSL)
  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.
}

我们可以看到,最终的结果 malloc_bytes 是通过 mallinfo 这个系统调用返回的,mallinfo 是一个用于获取内存分配信息的函数。它提供了有关堆内存(heap memory)的详细使用情况,包括已分配的内存块数量、空闲的内存块数量、内存碎片等信息。这个函数返回一个 struct mallinfo 类型的结构体,附上链接 ,这里面记录着内存的大多数信息,我们只拿 ART 用到的 uordblks ,与 hblkhd 解释一下:


ordblks


代表普通(非 mmap 分配的)空闲内存块的数量。这些空闲块可以在后续的内存分配请求中被使用。例如,当程序释放了一些之前通过 malloc 分配的内存时,这些内存块可能会被归类为普通空闲内存块,ordblks 的值就会相应地增加。


hblks


用于记录程序请求使用 mmap 分配大块内存(以 MMAP_THRESHOLD 为界,这个阈值可以通过系统参数等方式设置)的次数。当程序需要分配较大的内存块时,可能会使用 mmap 方式,每请求一次,hblks 的值就会增加。


这两个变量其实就是记录中我们在 Native 中常用的内存分配函数 malloc 系列( calloc 等)与 mmap 函数所分配的内存数量。


最终返回的结果是 malloc_bytes(mmapped_bytes > malloc_bytes 取两种中大的) + native_bytes_registered ,那么这个native_bytes_registered 又是个啥,这跟我们下文会讲到的NativeAllocationRegistry 有关,我们看下它的赋值,其实就是通过 RegisterNativeFree 函数的 bytes 参数得到的。


void Heap::RegisterNativeFree(JNIEnv*, size_t bytes) {
  size_t allocated;
  size_t new_freed_bytes;
  do {
    allocated = native_bytes_registered_.load(std::memory_order_relaxed);
    new_freed_bytes = std::min(allocated, bytes);
    // We should not be registering more free than allocated bytes.
    // But correctly keep going in non-debug builds.
    DCHECK_EQ(new_freed_bytes, bytes);
  } while (!native_bytes_registered_.CompareAndSetWeakRelaxed(allocated,
                                                              allocated - new_freed_bytes));
}

最终通过两者计算得出阈值,从而触发是否要产生 GC,那么 CheckGCForNative 会在哪里被触发呢?接下来看。


/   为什么ART要根据Native内存占用触发GC呢?   /


大家可能会问,为啥 GC 要判断 Native 内存大小触发呢?GC 回收的不是 Java 虚拟机的内存吗,跟 Native 内存有啥关系,Native 内存不是由使用者自己回收吗?其实这是有原因的。


在日常 JNI 开发中,我们通常都需要用到 Java 对象与 Native 对象,通过 Java 对象保存 Native 对象的指针是一个很常见的操作,比如 Bitmap。


public final class Bitmap {

  private final long mNativePtr;
  ....

外部通过 Java 对象去管理,内部比如内存的分配放到 Native 内存中。我们知道 Java 对象可以由虚拟机去回收,那么 Java 对象持有的 Native 对象怎么办呢?


在最早期的版本,开发者通过在 finalize 函数中主动调用释放所持有的 Native 内存,比如 Camera2 中比较典型的 CameraMetaDataNative,其实就是利用 finalize 方法进行最后的 Native 对象内存回收。


@Override
protected void finalize() throws Throwable {
    try {
        close();
    } finally {
        super.finalize();
    }
}

private void close() {
    // Delete native pointer, but does not clear it
    nativeClose(mMetadataPtr);
    mMetadataPtr = 0;

    if (mBufferSize > 0) {
        VMRuntime.getRuntime().registerNativeFree(mBufferSize);
    }
    mBufferSize = 0;
}


看似很合理对吧,但是我们也同样知道,一个垃圾对象的 finalize 方法其实是依赖 GC 的执行的,只有 GC 后才会被调用 finalize 方法,然而在早期的 android 系统中,GC 策略只会依据 Java 内存大小的容量进行,很有可能出现的情况是,即便一个对象处于无用状态,但是因为还没有达到 Java 内存的阈值,因此也不会进行GC,这就导致了其所持有的 Native 内存一直也是属于不释放的状态,造成无用的 Native 内存过多的问题。这类问题我搜了一下,发现还真的存在,比如 Android Camera 内存问题剖析。


同时系统大部分关键类也采取了上述的设计,因此官方在后续引入了一个叫NativeAllocationRegistry 的类,用于管理 Java 对象以及其所持有的 Native 内存,当用其管理内存时,会调用 registerNativeAllocation 方法,此时就会通过 JNI 方法(VMRuntime_registerNativeAllocation等)调用到我们上面说的CheckGCForNative方法。


private static void registerNativeAllocation(long size) {
  VMRuntime runtime = VMRuntime.getRuntime();
  if ((size & IS_MALLOCED) != 0) {
      final long notifyImmediateThreshold = 300000;
      if (size >= notifyImmediateThreshold) {
          runtime.notifyNativeAllocationsInternal();
      } else {
          runtime.notifyNativeAllocation();
      }
  } else {
      runtime.registerNativeAllocation(size);
  }
}

从而达到Native内存阈值检测的目的,即使触发一次GC,从而让依赖GC回收的对象尽快回收,从而让其持有的Native对象内存也一并被回收。这是一个非常大的改进,后续很多Android系统核心类都是用了这个策略,从而保证了Native内存的有效回收。


我们刚刚上文还提到一个native_bytes_registered 对象,它其实就是通过NativeAllocationRegistry传入的Native对象大小,因为NativeAllocationRegistry创建时可以让使用者提供它当前所持有的Native对象的大小,通过registerNativeFree方法,这样就不会错过使用者告知的Native内存,使得整个内存统计更加准确。


// Inform the garbage collector of deallocation, if appropriate.
private static void registerNativeFree(long size) {
    if ((size & IS_MALLOCED) == 0) {
        VMRuntime.getRuntime().registerNativeFree(size);
    }
}

值得一提的是,我们会在代码中看到IS_MALLOCED 这个标记,这个也是创建NativeAllocationRegistry时由使用者告知的,如果它是通过malloc所分配的,其size属性就会在最后一位加上,IS_MALLOCED 这个标记。


private NativeAllocationRegistry(@NonNull ClassLoader classLoader, @NonNull Class clazz,
    long freeFunction, long size, boolean mallocAllocation) {
    if (size         throw new IllegalArgumentException("Invalid native allocation size: " + size);
    }
    this.clazz = Objects.requireNonNull(clazz);
    this.classLoader = Objects.requireNonNull(classLoader);
    this.freeFunction = freeFunction;
    this.size = mallocAllocation ? (size | IS_MALLOCED) : (size & ~IS_MALLOCED);

    synchronized(NativeAllocationRegistry.class) {
        registries.put(this, null);
    }
}

/   我们可以做的性能优化   /


我们可以验证一下是不是 Native 内存过多时 NativeAllocationRegistry 是否按照正常的策略回收,我们可以通过 inline hook RequestConcurrentGC这个方法,去查看其 GcCause 是不是 kGcCauseForNativeAlloc 去验证我们整个流程,对应的符号是_ZN3art2gc4Heap19RequestConcurrentGCEPNS_6ThreadENS0_7GcCauseEbj。值得注意 RequestConcurrentGC 是 Heap 类的成员方法,因此其函数类型需要多加上 void *heap 这个 this 指针,不要忘记噢。


bool hookRequestConcurrentGC(void *heap, void *self,
                             enum GcCause cause,
                             bool force_full,
                             uint32_t observed_gc_num) {
    __android_log_print(ANDROID_LOG_ERROR, "hello""gc type %d", cause);
    bool result = ((gc_temp) heap_record_gc)(heap, self, cause, force_full, observed_gc_num);
    __android_log_print(ANDROID_LOG_ERROR, "hello""result ", cause);
    return result;
}

当我们分配一个大型 malloc 对象时,就会看到由系统的 NativeAllocationRegistry 触发的 gc,其 type 正是 kGcCauseForNativeAlloc。


值得注意的是,我们的创建的 malloc 对象并不会被回收,因为这个对象是我们自己管理的,会被回收的内存只是由无用 Java 对象所持有的 Native 内存。


通过上面我们了解到,在 Native 内存过多时触发一次 GC 是有好处的,能够加快一些无用的 Java 类回收,也能够保证其所持有的 Native 内存回收,因此在一些早期的 Android 版本之上或者是没有用到 NativeAllocationRegistry 进行管理的 Java 类中,我们是可以通过检测当前 Native 内存的大小,主动触发 GC 的操作去提高内存回收的效率。


/   总结   /


通过学习 GC 类型为 kGcCauseForNativeAlloc 的整个链路,能够帮助我们更加全面的认识 ART 的 GC 细节,同时其在高版本所用到的回收策略,也是可以被我们运用在低版本中,达到性能优化的目的。


推荐阅读:

我的新书,《第一行代码 第3版》已出版!

原创:写给初学者的Jetpack Compose教程,edge-to-edge全面屏体验

一个适用于触控笔应用的全新 Jetpack 库


欢迎关注我的公众号

学习技术或投稿



长按上图,识别图中二维码即可关注