拯救OOM~ 字节自研 Android 虚拟机内存管理优化黑科技 mSponge

news2024/11/15 7:16:17

本文描述的虚拟机内存管理优化方案,是从应用侧视角对 Android 虚拟机内存管理进行改造,优化了虚拟机对 LargeObjectSpace 的内存管理策略,间接增加其它内存空间使用上限。改造后的方案,32 位运行环境 LargeObjectSpace 的内存使用上限可达到 2G 甚至更多(64 位环境使用上限理论上会趋于无限大)。通过本方案可以最大程度上从系统侧解决诸多应用都会遇到的内存瓶颈和 OOM 问题,一键接入,安全可靠。

1.背景

Java OOM对于 Android 开发者来说并不陌生,随着应用愈发复杂,部分业务在设计之初为了更好的产品体验,往往会考虑用空间换时间,长此以往,有限的内存资源将会不堪重负,尤其是在一些重大活动期间,内存挑战会更加严峻,稍有不慎就会暴雷。

为了应对这种情况,开发同学往往会从应用层面进行优化,减少缓存,但随着产品持续迭代,内存问题始终如达摩克利斯之剑,让应用处于崩溃的边缘。好在 Android 系统维护人员也意识到了这种问题,从 Android O 系统开始,对 Java 内存管理策略进行了调整,重点包括实例化 Bitmap 对象时,Bitmap 源数据的内存不再通过虚拟机申请,而是直接在 Native 层申请和管理,因此这部分内存不会纳入虚拟机 Heap 内存统计,以此来减少 LargeSpace 的占用,进而间接增加了其他内存空间的实际使用范围(此消彼长的关系),如下示意图:

通过调整 Bitmap 的内存管理策略以减少虚拟机整体内存使用,的确带来了立竿见影的效果。以字节公司内部诸多 App 为例,Android O 之后的移动设备,Java OOM 远远低于早期的系统版本。

2.思考与破局

对于上述 Android 内存管理策略优化,可以看到其实只有 Android N 以上版本受益,但是市面上仍有大量的低版本设备需要关注,为了带给用户更好的体验,我们开始思考,既然在 Android O 系统上可以通过调整 Bitmap 内存管理策略,以降低 Java 内存触顶压力,那么针对 Android 低版本是不是也可以考虑通过同样的方式去转移内存统计策略呢?

经过一番探索,最终找到了我们想要的答案:在应用侧,我们实现了对虚拟机内存管理策略的改造,将 LOS(Large Object Space)的整个内存使用,从虚拟机的内存统计之中进行移除,以保障其它内存 Space 可以更大范围地申请内存。

3.背景知识介绍

在正式介绍该方案之前,有必要先来简单了解一下 Android 虚拟机的内存空间管理、Java 大对象管理以及内存申请流程,以便于我们更加清晰地理解该方案的设计思想。

3.1 内存空间分类

众所周知,系统在创建 Zygote 过程中会初始化虚拟机配置,其中一个便是设置 Heap 内存。默认 HeapMax 是 256M、512M,后续应用进程通过 Zygote 进程孵化时,都会继承该配置且无法修改。但对于虚拟机来说,为了更好地管理内存并提升分配和回收性能,并没有将所有 Java 对象全部放在一块空间进行管理,而是按照不同的场景属性划分成若干个内存空间,这些内存空间将会共享虚拟机 512M 最大内存,因此它们之间是一个此消彼长的关系,如下图:

同时虚拟机在 GC 过程中,可以将部分 Object 对象进行移动以降低内存碎片,因此根据内存对象是否支持移动分为可移动对象、不可移动对象;按照连续性分为连续性空间(ContinuousMemMapAllocSpace)和非连续空间(DiscontinuousSpace);最终每个子类的 Space 都继承至 Space 和 ContinuousSpace/DisContinuousSpace,从上而下的继承关系,如下图:

这些内存空间的实例化对象存储在虚拟机 Heap 对象中,申请内存时根据内存属性选择不同的内存空间进行分配和管理。

针对此文,我们重点关注的是大内存管理,以 LargeObjectMapSpace 为例,从上图可以看到它继承至非连续内存空间,对于非连续内存的管理要简单得多,简单总结如下:虚拟机默认把大于 3 个物理页(12K)的原子性对象或 String 类型的对象通过该空间进行管理,该空间所有的对象都存储在一个 Map 容器中,每次 GC 时遍历这些对象是否被引用,如果没有被引用则直接释放即可。由于这些大对象之间是离散的,因此不会造成内存碎片问题,在 GC 时也就无需进行拷贝压缩以释放连续空间。下面我们再简单介绍一下 LargeObjectSpace 的角色和工作方式。

3.2 大对象内存管理

Java 大对象的内存管理比较简单,主要集中在 LargeObjectSpace 模块,主要负责 Java 大对象的内存申请和释放。

3.2.1 内存申请

结合源码,接下来简单介绍一下 Java 大对象申请流程,整理流程图如下。在此过程中,重点关注 Heap 已申请内存大小的更新过程和内存不足时抛出 OOM 的过程

在上图我们可以看到,在内存实际申请过程,会首先检测当前对象是否满足大对象,具体条件是:申请对象类型必须是原子类型或者 String 类型,并且要大于 3 个物理页

inline bool Heap::ShouldAllocLargeObject(ObjPtr<mirror::Class> c, size_t byte_count) const {

  return byte_count >= large_object_threshold_ && (c->IsPrimitiveArray() || c->IsStringClass());

}

如果满足以上条件,则会执行大对象申请流程,从 LargeObjectMapSpace 进行申请。但是在正式申请之前,会再次判断是否内存触顶,计算规则是将 Heap 中已经申请的内存和将要申请的内存进行相加,判断是否超过虚拟机的内存上限(growth_limit_),如果没有超过则说明不会触顶,然后就会直接从 LargeObjectMapSpace 申请一块内存;如果大于 growth_limit_,则说明可能会触顶,此时要先进行 GC,争取释放一些内存,如果多轮 GC 之后仍然满足不了,则抛出 OOM

在通过以上检测之后,将会通过LargeObjectMapSpace::Alloc 实例化一个 Object 对象,如下图:

结合上图,可以看到 LargeObjectMapSpace 在内存申请过程中如要完成以下工作:

  1. 根据申请的内存大小,利用 mem_map 映射与之对应的一块内存;
  2. 将映射的内存转换为 mirror::Object 对象;
  3. 将该 Object 对象与 mem_map 实例进行关联,并存储到 large_objects 集合;
  4. 更新 largeObjectMapSpace 当前内存占用和对象数量,以及累计占用大小和对象数量;

3.2.2 内存释放

内存释放逻辑:根据传入的对象,先检查是否在 large_objects _ 集合中,如果不存在则抛出异常;否则同步更新(释放)当前内存状态,并从该集合移除该对象。

在介绍完虚拟机内存空间管理以及 Large Object Space 内存管理方式的相关背景知识后,接下来我们回归到正题,看看我们针对虚拟机是如何改造 Large Object Space 的内存管理策略的。

4. mSponge 实现原理

4.1 方案简介

为了便于更好地理解,我们将整个方案分为 2 个部分进行介绍。

一期方案:主要介绍在 Java 大对象通过 LargeObjectSpace 的内存申请和释放过程中,如何在内存申请和释放过程对其进行改造,以脱离虚拟机对这些对象的内存管理,最后实现 LargeObjectSpace 占用的内存完全脱离虚拟机内存统计。

二期方案:针对一期方案需要在应用运行过程中提前开启,但是线上 99%以上运行过程中可能不会发生 OOM,因此一期方案对系统的侵入有点高。为了优化这个现象,二期方案主要通过监听应用是否发生 OOM,如监测到 OOM 则拦截并同开启一期方案“释放更多可用内存”,然后重试内存申请,以挽救本次 OOM;如果没有发生 OOM,则说明内存状态良好,该方案就不需要开启。显然,这种智能式的开启方式和最大化的内存保障效果将是更加极致的解决方案。

4.2 命名由来

在运行过程中,该方案会随着 LargeObjectSpace 的使用情况动态“吸收”和“释放”虚拟机 Heap 统计内存——“吸收”不希望被虚拟机统计的 LargeObjectSpace 的内存,“释放”已经通过 GC 回收的 Large Object 内存。整个运行过程犹如海绵吸水,故将该方案命名为:Memory Sponge,寓意:内存海绵,简称mSponge

4.3 mSponge 一期

从上面的大内存申请流程图中可以看到,如果当前内存申请满足 Java 大对象的条件(大于 12K),并在内存申请过程检测是否内存触顶时,“一直”返回 False,则可以通过 LargeObjectMapSpace 直接申请并返回对象实例,则可以绕过这里的内存触顶 OOM 问题。

同时,我们知道LargeObjectMapSpace 内部管理的对象是离散的,不支持虚拟机 GC 过程中连续内存空间的拷贝压缩的特性,因此即使是该空间内存占用过多,导致总内存超过了上限(512M?),但是其它连续内存 Space 的内存阈值仍然保持正常范围,因此不会影响到其他内存空间 GC 同构拷贝压缩能力,也就不会破坏虚拟机的内存管理。

4.3.1 方案思路

那么如何才能在大对象内存触顶检测过程中绕开现有的检测机制呢?通过调研发现判断内存触顶的关键条件在于虚拟机中管理当前已申请内存 Heap::num_bytes_allocated_对象,即每次内存申请成功和 GC 释放时,都会同步更新该值:

inline mirror::Object* Heap::AllocObjectWithAllocator(Thread* self,
ObjPtr<mirror::Class> klass, size*t byte_count, AllocatorType allocator, const PreFenceVisitor& pre_fence_visitor) {
......  
 //实际申请内存过程
......  
 if (bytes_tl_bulk_allocated > 0) {
size_t num_bytes_allocated_before =
//成功申请之后,需要同步更新虚拟机整体 Heap 内存使用
num_bytes_allocated* . fetch_add ( bytes_tl_bulk_allocated , std :: memory_order_relaxed );
......
}
}

GC 过程中,当每个 Space 释放一定对象和内存之后,会进一步同步到虚拟机的 Heap 对象,同步更新虚拟机整体内存使用,接口如下:

void Heap::RecordFree(uint64_t freed_objects, int64_t freed_bytes) {
  ......
  // Note: This relies on 2s complement for handling negative freed_bytes.
  //释放之后,需要同步更新虚拟机整体Heap内存使用
  num_bytes_allocated_  . fetch_sub (static_cast< ssize_t >( freed_bytes ), std :: memory_order_relaxed );
  ......
}

通过上面这个接口我们可以看到,每个 Space 在内存回收后都会更新虚拟机更新整体内存使用情况,那么我们是不是可以在合适的时机人为主动调用该接口,减去 LargeObjectMapSpace 管理的内存值,那么 Heap::num_bytes_allocated_统计的就全部是其他内存 Space 的内存使用了;换而言之通过 LargeObjectMapSpace 申请的内存将会“脱离虚拟机”Heap::num_bytes_allocated_的统计,纳入 Native 层的内存管理。但是这些对象的引用和内存回收机制仍然由虚拟机管理,因此并不会存在内存泄漏的隐患。

4.3.2 流程示意

LargeObjectMapSpace申请的内存,直接通过 Map 映射到虚拟内存,因此对于 32 位环境应用空间可映射内存在 3G 左右,但虚拟机本身会抢先占用 1G+的地址空间用于管理 Java 内存,因此应用侧实际使用范围在 2G 左右,极端情况下调整后的虚拟机内存理论范围将在 512M~2.5G,至于下限为何是 512M?理论上如果发生 OOM 时虚拟机没有任何大对象,这种情况下,则虚拟机可用内存范围将保持不变,因为我们改变的 Java 大对象的内存可用空间;示意图如下:

4.3.3 关键实现

上面介绍了该方案的背景知识和实现思路,接下来就要从技术层面考虑如何去实现了。如果在系统层面,直接从源码层面定制,相关改动会轻松很多,但是对应用侧来说,要想兼容不同 Android 版本,只有一条路可走——通过 InlineHook 代理相关接口,在执行过程中魔改相关参数以达到目的。在解决完接口代理问题之后,接下来还有下面几件事情要解决:

  • 虚拟机并没有对外暴露获取 LargeObjectMapSpace 内存的接口,如何才能实时获取当前 Space 已申请的内存大小?
  • 如何在合适的时机同步 Heap::num_bytes_allocated_内存统计,以便于让 LargeObjectMapSpace 的内存"脱离"虚拟机的统计?
  • 如何"跳过"虚拟机在内存释放过程对内存大小一致性校验的问题?
4.3.3.1 获取 LargeObjectMapSpace 当前内存

针对第一个问题,尽管 LargeObjectSpace 中提供了获取当前内存大小的接口(LargeObjectSpace::GetBytesAllocated),但是这个接口并没有对外暴露,因此需要通过解析 Libart.so 中的"GetBytesAllocated"符号,以 Android Q 为例,该函数签名符号为: _ZN3art2gc5space16LargeObjectSpace17GetBytesAllocatedEv;并在运行过程中动态获取该符号在内存中的地址。

由于 GetBytesAllocated 是非静态函数,因此在实际调用该接口时,需要知道当前对象的实例化对象,然后通过实例化对象调用该接口即可,在这里,我们通过 inlineHook 代理"LargeObjectMapSpace::Alloc"获取 LargeObjectMapSpace 的实例,LargeObjectMapSpace::Alloc 接口部分源码如下:

mirror::Object* LargeObjectMapSpace::Alloc(Thread* self, size_t num_bytes, size_t* bytes_allocated, size_t* usable_size, size_t* bytes_tl_bulk_allocated) {

  std::string error_msg;

  MemMap mem_map = MemMap::MapAnonymous("large object space allocation",
                                        num_bytes,
                                        PROT_READ | PROT_WRITE,
                                        /*low_4gb=*/ true,
                                        &error_msg);
  ......
  //申请成功后将当前内存占用+ allocation_size
  num_bytes_allocated_  += allocation_size ;
  total_bytes_allocated_ += allocation_size;
  ++ num_objects_allocated_  ;  //申请成功后将当前内存数量+1
  ++total_objects_allocated_;
  return obj;
}

在获取 LargeObjectMapSpace 的实例化对象之后,再通过该对象直接调用 GetBytesAllocated即可实时获取当前 LargeObjectMapSpace 的内存大小

4.3.3.2"移除"LargeObjectSpace 内存

当我们可以实时获取 LargeObjectSpace 的内存使用之后,接下来便是如何从虚拟机 Heap 中“移除”LargeObjectSpace 实际占用的内存了,通过调研发现可以通过解析"Heap::RecordFree"函数符号,并调用Heap::RecordFree 的函数接口,增加或减少 Heap 中我们想要更新的内存大小

void Heap::RecordFree(uint64_t freed_objects, int64_t freed_bytes) {
  ......
  // Note: This relies on 2s complement for handling negative freed_bytes.
  num_bytes_allocated_.fetch_sub (static_cast< ssize_t >( freed_bytes ), std :: memory_order_relaxed );
  ......
}

当上述条件满足我们可以灵活更新虚拟机 Heap 内存之后,接下来要处理的就是选择一个合适的时机直接“移除”LargeObjectSpace 的内存,并且需要更新记录当前 Heap“移除”的内存大小,经过调研并考虑到及时性和精准性,最终选择了在 LargeObjectSpace 的内存申请和回收过程,对虚拟机 Heap 内存进行动强制更新,以移除虚拟机对 LargeObjectSpace 的内存统计。

在 Large Object 申请过程,如果内存申请成功,则在该 Object 实例化对象返回之前,先强制从虚拟机内存统计中减去该部分内存,接下来虚拟机内部会在返回实例化对象之后,并统计本次新增内存,在这里我们通过先减后加的方式,维持了整个内存水位不变,从而间接地实现了虚拟机“忽略”了本次内存开销。

如果后续 GC 过程中释放了 LargeObjectSpace 中的部分或者全部对象,正常情况下释放的内存会同步同步到 Heap,以便于更新整体使用内存及可用内存,但是从上面的分析中我们知道,其实 LargeObjectSpace 的内存已经不在 Heap 统计之中了,如果从 Heap 中减去释放的这些内存,那么将会导致 Heap 统计的内存偏少,因此需要主动将该部分释放的内存"补偿"回来,避免统计错乱。

通过上述步骤实现了在内存回收过程中对大对象内存管理的改造,改造之后 Heap 统计的内存将不再包含 LargeObjectSpace 管理的内存,从而间接地扩大了其他内存 Space 使用上限;对于 LargeObjectSpace 来说,虚拟机统计到的该内存 Space 一直为 0,但是 LargeObjectSpace 内部并没有内存限制,异常该 Space 的内存使用上限将会显著提升,针对 Android O 以下系统来说,这部分内存 Space 不仅包含 Bitmap,还包含其他大对象。

4.3.3.3 内存校验

在适配过程中,发现 Android L 版本之后,虚拟机会在 GC 过程中对释放内存和使用内存进行一次校验。如果发现当前使用内存加上释放内存小于 GC 之前的内存,则会抛出“断言”异常,相关源码如下:

void Heap::GrowForUtilization(collector::GarbageCollector* collector_ran,

                              uint64_t bytes_allocated_before_gc) {

   //GC结束后,再次获取当前虚拟机内存大小

  const uint64_t bytes_allocated = GetBytesAllocated();
  ......
 if (!ignore_max_footprint_) {
        const uint64_t freed_bytes = current_gc_iteration_.GetFreedBytes() +
          current_gc_iteration_.GetFreedLargeObjectBytes() +
          current_gc_iteration_.GetFreedRevokeBytes();
    //GC之后虚拟机已使用内存加上本次GC释放内存理论上要大于等于GC之前虚拟机使用的内存,如果不满足,则抛出Fatel异常!!!
  CHECK_GE ( bytes_allocated + freed_bytes , bytes_allocated_before_gc );
 }
  ......

}

因为我们在内存 GC 过程中,动态调整了 Heap 当前使用内存大小,这可能会导致 gc 结束后再次获取的 Heap 当前使用内存小于实际值,为了不影响校验逻辑,需要代理 Heap::GrowForUtilization 接口,强制将bytes_allocated_before_gc 参数设置为 0,以保证校验恒成立(针对该处调整从后续逻辑和实际测试来看,对后续内存 GC 并无明显影响)

4.3.4 小结

至此,通过上述思路和技术方案完成了 Android 虚拟机内存统计策略的改造,该方案不仅间接提升了虚拟机其它内存空间运行时的使用上限,也将 LargeObjectSpace 的内存使用上限完全脱离了虚拟机的限制,完全等同于 Native 内存属性进行管理。因此该方案相比 Android 系统 Bitmap 内存管理改造更加彻底,给应用的内存环境带来了极大的改善。

4.4 mSponge 方案二期

在上文,对内存统计策略改造之后可以很大程度释放 LargeObjectSpace 内存空间以优化 Java OOM 问题,但是进一步思考之后,发现一期方案并不是最优解,因为应用运行过程中很大概率不会发生 OOM,如果能监听到 OOM 时,再启动优化方案,同时再救活本次 OOM,那么这种智能化的按需开启将是极致化的解决方案

4.4.1 方案思路

针对上述思考,基于 mSponge 方案一期的设计,决定采用“OOM 探测+按需开启”的策略来完成对内存的按需扩展。即:对内存申请过程进行定向监控,当监听到内存不足即将抛出 OOM 异常时,进行拦截,并激活 mSponge 方案一期内存优化方案(从 Heap 内存统计中移除当前 LargeObjectSpace 使用内存);然后再触发一次内存申请,以保证内存成功申请。按照上述思路,整理方案优化前后对比示意图:

4.4.2 流程示意

基于上面的思路,我们需要在虚拟机内部监听并拦截 OOM,当监听到第一次 OOM 时主动将 LargeObjectSpace 的内存从 Heap 统计中移除,以增加空闲内存,同时再开启 mSponge 一期优化策略,以保证后续 LargeObjectSpace 的内存变化不会影响 Heap 内存统计;按照这种思路,整体二期方案示意图如下:

4.4.3 关键实现

二期技术实现主要涉及下面几个流程:监听并判断是否需要拦截 OOM 异常;监听内存分配结果;重试内存申请。具体如下:

  • 监听 OOM 异常:代理 Heap::ThrowOutOfMemoryError,监听内存分配失败时抛出 OOM 的过程,并判断是否需要拦截,如果可拦截。
  • 监听内存分配结果:代理 Heap::AllocateInternalWithGc ,监听本次内存申请过程中,是否发生了 OOM 并被拦截,如果发生 OOM 并被拦截,则再次触发内存申请,以保证内存申请成功。
  • 重试内存申请:通过 AllocateInternalWithGc 再次触发一次内存申请,并在此之前禁止拦截本次内存申请过程中可能抛出的 OOM,如果成功申请,则返回该对象。
4.4.3.1 监听 ThrowOutOfMemoryError

通过 inlineHook 代理"Heap::ThrowOutOfMemoryError"并监听该接口,如果该接口被调用则说明,当前内存不足或者没有连续内存,无法满足本次内存需求;并根据调用 AllocateInternalWithGc 代理接口设置的标识"sAllowsSkipThrowOutOfMemoryError",判断是否拦截本次 OOM 异常;实现如下:

void ThrowOutOfMemoryErrorProxy(void* heap, void* self, size_t byte_count, AllocatorType allocator_type){
    if(isAllowWork()) {
        sFindThrowOutOfMemoryError = true;
    if (sAllowsSkipThrowOutOfMemoryError
 && !sForceAllocateInternalWithGc) {
             //拦截并跳过本次OutOfMemory,并置标记位
            sSkipThrowOutOfMemoryError = true;

            //TODO:将LargeObjectSpace内存从Heap中移除
            return;
        }
        sSkipThrowOutOfMemoryError = false;

         //如果不允许拦截,则直接调用原函数,抛出OOM异常
        ThrowOutOfMemoryErrorOrigin(heap, self, byte_count, allocator_type);
    } else{
        ThrowOutOfMemoryErrorOrigin(heap, self, byte_count, allocator_type);
    }
}
4.4.3.2 代理 AllocateInternalWithGc

通过 inlineHook 代理"Heap::AllocateInternalWithGc"代理并监听该接口,因为在该接口执行过程中会触发一次或多次 GC,如果依然满足不了本次内存申请,则会抛出 OOM 并返回 NULL;因此可以通过代理该接口可以知道本次内存申请是否成功,以及本次申请过程中是否抛出 OOM 异常;如果返回对象为 NULL,并拦截了 OOM 异常,则设置禁止拦截 OOM 标记之后,调用 Heap::AllocateInternalWithGc 原接口再次进行内存申请,以保证成功申请内存。

原则上通过 mSpnge 方案将 LargeObjectSpace 的内存从 Heap 移除之后,理论上虚拟机可用内存会增加很多,基本能保证本次内存成功申请(极端情况仍会出现内存不足,正常抛出 OOM 即可)。

void* AllocateInternalWithGcProxy(void* heap, void* thread,
                                  AllocatorType allocator,
                                  bool instrumented,
                                  size_t alloc_size,
                                  size_t* bytes_allocated,
                                  size_t* usable_size,
                                  size_t* bytes_tl_bulk_allocated,
                                  void* klass){
    if(isAllowWork()) {
         //设置标记位,允许拦截本次内存申请过程的OOM
        sAllocateInternalWithGc = true;

        sForceAllocateInternalWithGc = false;

        //调用原始接口,并判断返回object是否未NULL
        void *object = AllocateInternalWithGcOrigin(heap, thread, allocator, instrumented, alloc_size, bytes_allocated, usable_size, bytes_tl_bulk_allocated, klass);
        sAllocateInternalWithGc = false;
          //如果返回object为NULl,并且在此过程中抛出了OOM异常,则说明内存不足导致申请失败,则开启虚拟机内存统计策略优化方案,释放LargeObjectSpace内存
        if (object == NULL && sAllowsSkipThrowOutOfMemoryError && sSkipThrowOutOfMemoryError) {

             //设置标记位,不允许拦截本次内存申请过程的OOM
            sForceAllocateInternalWithGc = true;

             //再次调用内存申请,争取成功申请内存
            object = AllocateInternalWithGcOrigin(heap, thread, allocator, instrumented, alloc_size, bytes_allocated, usable_size, bytes_tl_bulk_allocated, klass);

            sForceAllocateInternalWithGc = false;
        }
        return object;
    } else{

        return AllocateInternalWithGcOrigin(heap, thread, allocator, instrumented, alloc_size,
                                     bytes_allocated, usable_size,
                                     bytes_tl_bulk_allocated, klass);
    }
}

4.4.4 小结

至此,通过上述思路和技术方案完成了应用内存申请过程中的 OOM 监测,并在虚拟机内存抛出 OOM 的过程对 Heap 的内存管理进行了改造,移除了 LargeObjectSpace 的内存占用,间接增加了虚拟机可用内存之后,再次触发内存申请,以拯救本次 OOM;通过这种按需开启的方式,体现了以最小化的侵入成本换来最大化的内存保障。

5.方案收益

以今日头条 32 位测试环境为例,通过该方案对虚拟机大对象内存扩展了 500M(实际使用可根据产品自身特点设置扩展内存大小),通过测试用例连续占用 500M 内存后,虚拟机内存分布如下图:

从上图可以清晰看到虚拟机其它内存空间(main space)仍然是 1G 大小,但是large space 内存大小接近 600M,内存可用范围得到明显增加

5.1 统一 Large Object 管理策略

通过该优化方案,实现了在应用侧对 Android 各版本虚拟机内存管理策略的统一,弥补了 Android 系统优化向下兼容性不足的缺陷,很大程度降低了产品设计和 RD 开发过程,需要对低版本内存管理差异性进行降级或兼容的成本。

5.2 OOM 收益

统计近半年头条线上 OOM Case(集中发生在 Android O 以下版本,因为低版本 Bitmap 内存纳入虚拟机管理和统计,更加容易导致内存触顶),都发生在部分图片缓存过高,或者各种节日运营导致 Bitmap 缓存不合理;由于该方案将虚拟机大内存的统计进行了优化,相当于“扩展”了虚拟机可用内存,因此对于上述场景导致的 Java OOM 问题,起到很好的容错能力,挽救 90%以上的 OOM 问题。

6.总结

通过上文,我们以 32 位运行环境为例,介绍了Android 虚拟机内存管理策略的改造思路,改造之后的内存管理策略,统一了 Android 系统碎片化的内存管理差异,进一步优化应用的运行环境,显然更加符合应用侧对 Java 内存的使用诉求,更好地应对和保障了应用运行时的稳定性,带给用户更好体验。

7.后续

在移动互联网快速迭代的背景下,各类应用也在快速迭代,如何更好地保障线上质量,将是一种长期需要应对和探索的方向,除了在常规视角进行优化之外,如何进行更深层次的系统探索,也是我们日常工作的主要方向,后续我们将会分享更多关于系统层面的相关实践。


根据上述中的Android 虚拟机内存管理优化分析,下面针对性能优化知识点进行整理,大家可以进行参考《性能优化学习手册》

《性能优化学习手册》:https://0a.fit/dNHYY

启动优化、UI布局优化、卡顿优化、布局优化、崩溃优化、网路优化、大图加载优化、存储优化、APK瘦身优化等……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/188680.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Aspose.CAD for .NET 23.1.0 Crack

Aspose.CAD for .NET 是一个独立的类库&#xff0c;它增强了 Windows 和 ASP.NET 的应用程序&#xff0c;以便在不需要 AutoCAD 甚至任何其他工作流渲染过程的情况下处理和渲染 CAD 绘图。CAD 类库允许将 DWG、DWT、DWXF、IFC、PLT、DGN、OBJ、STL、IGES、CFF2 文件以及图层和布…

封装一个顺序栈,并封装其相应的操作:判空、入栈、出栈、遍历栈、销毁

main.cpp#include <iostream> #include<fei1.h>using namespace std;int main() {der L;//创建L.date(215);//入栈L.ent(45);L.ent(23);L.ent(98);L.ent(12);//遍历L.trav();// 出栈L.come();L.come();//遍历L.trav();//销毁L.dest();return 0; }fei.cpp#include&l…

docker学习(三):docker的常用命令问问

文章目录前言docker镜像分层加载原理docker镜像commit操作产生新镜像本地镜像发布到阿里云将本地镜像推送到私有库前言 大家好&#xff0c;这是我学习docker系列的笔记文章&#xff0c;目标是掌握docker,为后续学习K8s做准备。本文记录了docker镜像分层加载的原理&#xff0c;…

IDEA SpringBoot热部署

IDEA SpringBoot热部署【自动帮开发者重启 SpringBoot项目&#xff0c;以达到】 1.添加SpringBoot热部署框架支持 在pom.xml中添加如下框架引用&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devt…

Smart HTML Elements R1 2023

Smart HTML Elements R1 2023 增加了Angular 15支持。 添加了对Blazor的Microsoft.NET 7支持。 添加了三维图表组件。 添加了对网格保存/加载状态&#xff08;持久状态&#xff09;的支持。 调度程序 定义特定时段的可用性。 现在可以限制用户输入。 当有多个图例时&a…

长沙23岁应届生做软件测试1年,月入10k!室友悔不当初!

先简单的介绍一下自己&#xff0c;大家好&#xff01;我叫做程双喜&#xff0c;今天23&#xff0c;2021年大学毕业于长沙的一所大专学校&#xff0c;专业是软件测试java专业&#xff0c;三年时间过得很快&#xff0c;转眼间来到了毕业季&#xff0c;自己还是一无所成&#xff0…

Hadoop

1 Hadoop常用端口号 hadoop2.xHadoop3.x访问HDFS端口500709870访问MR执行情况端口80888088历史服务器1988819888客户端访问集群端口90008020 2 Hadoop配置文件 hadoop2.x core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml slaves hadoop3.x core-site.xml、hdfs…

ESP-IDF在VSCode中创建工程文件

1.新建工程项目&#xff0c;打开VSCode&#xff0c;打开命令面板&#xff0c;并输入esp-idf new&#xff0c;执行新建esp-idf项目命令&#xff1a; 2.设置项目名&#xff0c;项目工程目录位置&#xff0c;芯片类型&#xff0c;端口号 3.点击choose Template&#xff0c;从一个…

第八届蓝桥杯省赛 C++ A/B组 - 分巧克力

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;蓝桥杯题解集合 &#x1f4dd;原题地址&#xff1a;后缀表达式 &#x1f4e3;专栏定位&#xff1a;为想参加蓝桥杯的小伙伴整理常考算法题解&#xff0c;祝大…

全志A40i+Logos FPGA开发板(4核ARM Cortex-A7)硬件说明书(下)

前 言 本文档主要介绍板卡硬件接口资源以及设计注意事项等内容,测试板卡为创龙科技旗下的全志A40i+Logos FPGA开发板。 核心板的ARM端和FPGA端的IO电平标准一般为3.3V,上拉电源一般不超过3.3V,当外接信号电平与IO电平不匹配时,中间需增加电平转换芯片或信号隔离芯片。按键…

1、语义软分割算法(Semantatic Human Matting)原理及pytorcch代码详述

一、Semantatic Human Matting原理 Semantatic Human Matting 文章链接请点击这里 第一步:将输入图像送入到T-Net中进行计算,得到三张图:Bs、Fs和Us. T-Net:就是一个编码和解码的过程,和常见的语义分割网络类似 Bs:确定的背景区 Fs:确定的前景区 Us: 不确定的区域 第二…

计算机网络详解--套接字编程

目录 1.什么是网络编程 2.TCP/IP协议 3.Socket套接字 流套接字:使用传输层TCP&#xff08;传输控制协议&#xff09; 数据报套接字:使用传输层UDP&#xff08;用户数据报协议&#xff09; 原始套接字 4.Java数据报套接字通信模型 UDP数据报套接字编程 DatagramSocket A…

IDEA集成Docker配置

首先开启Docker的SSH连接&#xff0c;设置Docker允许远程连接sudo vim /lib/systemd/system/docker.service将ExecStart/usr/bin/dockerd -H fd:// --containerd/run/containerd/containerd.sock注释替换为ExecStart/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run…

GP232RL替代FT232RL国产USB转串口/UART芯片

GP232RL为接口转换芯片&#xff0c;可以实现USB到串行UART接口的转换&#xff0c;也可转换到同步、异步Bit-Bang接口模式。具备可选择的时钟产生输出&#xff0c;以及新的FTDIChip-ID安全加密狗功能。 此外&#xff0c;还提供异步和同步bit bang接口模式。使用GP232RL的USB到串…

用Flutter实现GaiaControl BLE OTA升级功能,支持Android/IOS

代码基本移植官方GaiaControl Demo。 支持RWCP 断点续传 设置蓝牙mtu.协议。这里主要分析GAIA CSR ble ota的过程&#xff0c;协议等等&#xff0c;希望对你有所帮助。这里对蓝牙服务特性订阅都不谈。读者自行了解。 Gaia 是CSR 制定的一个上层使用协议&#xff0c;其在BR/EDR…

Swagger的使用

一、概述 RestFul Api文档在线自动生成工具 >Api文档与API定义同步更新直接运行&#xff0c;可以在线测试API接口支持多种语言:&#xff08;Java,Php&#xff09; 官网&#xff1a;https://swagger.io/ 二、使用 在项目中使用Swagger需要springfox&#xff1b; Swagger…

Axure 原型中的迭代设计

​“老师&#xff0c;什么是产品迭代&#xff1f;” “老师。产品迭代和平常的产品设计有什么不同吗&#xff1f;” “老师&#xff0c;产品迭代原型可以怎么做&#xff1f;需要全部重新绘制吗&#xff1f;” 在和小伙伴们的日常交流中&#xff0c;不乏对产品迭代存在疑惑的朋…

2023年,数据人谨记把握好这“四不要”和“四要”

2023年已经开启&#xff0c;这一年对任何组织数据治理的工作都是非常重要的&#xff0c;那么我们如何更好的掌握数据治理和应对数据治理项目就成为重中之重&#xff0c;下面就和大家谈谈数据治理学习和项目的一些心得体会&#xff0c;供大家参考。 不要相互割裂&#xff0c;要融…

VS Code配置snippets代码片段快速生成html模板,提高前端编写效率

先看下示例&#xff0c;在输入 ! 号回车后自动生成一段代码片段。 这样我们就可以更便捷的进行代码编写了。 配置方法如下&#xff1a; 然后找到对应的文件进行配置&#xff0c;例如 html.json&#xff0c;编写 .html 扩展名文件时就能触发。 我这选的 html.json 进行的配置…

CSS选择器整理学习(下)

书接上回&#xff0c;在前端项目开发中&#xff0c;有时候需要对特殊的元素进行特殊的处理&#xff0c;但有时候元素的位置不确定、层级不确定、数量不确定等问题&#xff0c;导致我们没办法进行元素的选择&#xff0c;这个时候我们就需要用到元素选择器了。 一、css选择器 1…