手机虚拟机,手机虚拟机软件
在深入 Android 系统的内存管理机制后,我们发现虚拟机内存管理对于应用性能有着至关重要的影响。我们提出了一种全新的虚拟机内存管理优化方案,从应用侧视角对 Android 虚拟机的内存管理进行了大刀阔斧的改革。
众所周知,Java OOM 问题在 Android 开发中屡见不鲜。随着应用的复杂度不断提升,部分业务为了追求更好的用户体验,往往会采取空间换时间的策略,导致内存资源迅速耗尽。特别是在一些重大活动期间,内存挑战更是严峻,稍有不慎就会引发应用崩溃的问题。为了解决这一问题,我们从应用侧入手,重新优化了虚拟机对 LargeObjectSpace 的内存管理策略。
一、背景知识介绍
在 Android 系统中,虚拟机的内存管理至关重要。系统创建 Zygote 进程时会初始化虚拟机配置,其中包括设置 Heap 内存。为了更好地管理内存并提升分配和回收性能,虚拟机并不会将所有 Java 对象放在一块空间进行管理。相反,它会根据不同的场景属性将内存划分成若干个空间,这些空间共享虚拟机的最大内存,它们之间的关系是此消彼长的。
二、方案介绍
我们的优化方案主要是调整了 LargeObjectSpace 的内存管理策略。通过改造虚拟机对 LargeObjectSpace 的管理方式,我们将这部分内存的使用从虚拟机的统计中移除,从而间接增加了其他内存空间的使用上限。这一改动对于 32 位运行环境来说,LargeObjectSpace 的内存使用上限可以达到 2G 甚至更多;而对于 64 位环境来说,使用上限理论上会趋于无限大。
三、方案实施
在实施过程中,我们借鉴了 Android O 系统对 Java 内存管理策略的调整思路。特别是在实例化 Bitmap 对象时,我们调整了其内存管理方式。在改造前,Bitmap 源数据的内存是通过虚拟机申请的,这会导致 LargeSpace 占用较多。为了降低 Java 内存触顶压力,我们从应用侧出发,调整 Bitmap 的内存管理策略,将这部分内存的申请和管理转移到 Native 层,这样这部分内存就不会纳入虚拟机 Heap 内存的统计中。这样一来,就间接增加了其他内存空间的实际使用范围。
四、方案效果
通过实际应用测试,我们发现该方案效果显著。以字节公司内部的应用为例,在 Android O 之后的移动设备上,Java OOM 的情况远远低于早期系统版本。这不仅提高了应用的稳定性,也大大提升了用户体验。
五、思考与展望
虽然我们的方案已经取得了一定的效果,但市面上仍有大量低版本设备需要关注。为了带给用户更好的体验,我们仍在继续如何进一步优化虚拟机的内存管理策略。我们希望通过不断的研究和尝试,找到更多有效的解决方案,为 Android 用户带来更加流畅、稳定的体验。大内存管理与LargeObjectMapSpace的
当我们内存管理时,LargeObjectMapSpace成为了一个无法忽视的关键点。它处理非连续内存空间,特别是对于大于3个物理页(即12K)的原子性对象或String类型的对象的管理。这些大对象存储在Map容器中,每次进行垃圾回收(GC)时,都会检查这些对象是否被引用,如果没有被引用则直接释放。由于这些大对象之间是离散的,因此不会引发内存碎片问题,使得垃圾回收过程更为高效。
接下来,我们将更深入地LargeObjectSpace的角色及其工作方式。
一、大对象内存管理概述
在Java中,大对象的内存管理相对简单,主要由LargeObjectSpace模块负责。这个模块主要负责Java大对象的内存申请和释放。
二、内存申请流程
在内存申请过程中,首先会检测当前对象是否满足大对象的条件。具体来说,申请的对象必须是原子类型或String类型,并且其大小要大于3个物理页。如果满足这些条件,就会执行大对象的申请流程,从LargeObjectMapSpace进行申请。
在正式申请之前,系统会判断是否已经触顶。这一判断是基于已申请内存和将要申请的内存总和是否超过虚拟机的内存上限。如果没有超过,系统会从LargeObjectMapSpace直接申请一块内存;如果超过,系统会先进行垃圾回收(GC),试图释放一些内存。如果经过多轮GC后仍然无法满足需求,系统就会抛出OOM(内存溢出)异常。
在通过检测后,LargeObjectMapSpace会通过Alloc实例化一个Object对象。在这个过程中,需要完成一系列工作,包括映射内存、转换对象为mirror::Object类型、关联实例等。
三、内存释放流程
对于内存释放,系统会先检查待释放的对象是否在large_objects集合中。如果存在,就会同步更新(释放)当前内存状态,并从集合中移除该对象。
四、mSponge实现原理
为了更高效地管理LargeObjectSpace的内存,我们提出了mSponge方案。整个方案分为两期进行介绍。一期方案主要关注在Java大对象通过LargeObjectSpace进行内存申请和释放的过程中,如何对其进行改造,使其脱离虚拟机对这些对象的内存管理。我们的目标是将LargeObjectSpace占用的内存完全脱离虚拟机的统计,以实现更为灵活和高效的大内存管理。
LargeObjectMapSpace在大内存管理中扮演着关键角色。通过对其申请和释放流程的深入理解和改造,我们可以实现更为高效和灵活的大内存管理策略,以满足不断变化的应用需求。经过深入研究并吸取一期方案的实践经验,我们提出了更为先进的二期方案“Memory Sponge”,致力于在保障系统稳定性的基础上进一步优化内存管理。鉴于一期方案在应用运行过程中需要提前开启,并对系统产生一定的侵入性,我们决定采取更为智能的策略。二期方案的核心在于实时监测应用是否发生OOM(内存溢出)。一旦监测到OOM情况,立即启动一期方案的“释放更多可用内存”机制,同时拦截并处理内存申请失败的情况。通过这种方式,我们能够及时挽救潜在的OOM风险,确保系统的稳定运行。
关于命名由来,“Memory Sponge”的灵感来源于其在内存管理过程中的动态特性。当LargeObjectSpace(大对象空间)被使用时,我们的方案会如同海绵吸水一般动态地吸收和释放虚拟机Heap统计内存。具体来说,“吸收”指的是跟踪并管理LargeObjectSpace的内存使用情况,而“释放”则是针对已经通过GC(垃圾回收)机制回收的大对象内存进行智能管理。这一形象的命名方式,简洁地描述了方案的特性和功能。
进入更为细致的解读,我们的mSponge一期方案主要针对大内存申请流程进行优化。当满足Java大对象的申请条件(如大于12K的内存需求)时,我们会在内存申请过程中检测是否接近或达到内存上限。如果检测结果显示内存并未触顶,我们将通过LargeObjectMapSpace直接申请并返回对象实例,从而绕过潜在的OOM问题。这一决策背后的逻辑在于LargeObjectMapSpace管理的对象具有离散性,不会影响到虚拟机GC过程中的连续内存空间特性。即使该空间内存占用过多,也不会对其他内存空间的GC操作造成干扰。
那么,如何在大对象内存触顶检测过程中巧妙地绕开现有机制呢?我们深入研究了Java虚拟机的内存管理机制,并结合一期方案的实践经验,提出了一种创新的思路。我们将通过深入分析虚拟机的内存分配和回收机制,寻找在不影响系统稳定性的前提下,巧妙地绕开触顶检测机制的方法。这将涉及到对Java内存模型、垃圾回收算法以及系统架构的深入理解与创新应用。我们相信,通过不断的和实践,我们终将找到一种既能保障系统稳定性,又能优化内存管理的最佳解决方案。
通过调研,我们发现判断内存触顶的关键指标在于虚拟机中管理当前已申请内存的Heap::num_bytes_allocated_对象。每次内存申请成功和GC释放时,这一数值都会同步更新。这一机制确保了虚拟机的内存使用情况的准确性。
在虚拟机的内存分配过程中,特别是在Heap::AllocObjectWithAllocator这个方法里,实际申请内存的步骤被详细阐述。当成功申请内存后,一个关键的操作是同步更新虚拟机整体的Heap内存使用。这其中涉及到一个关键操作:num_bytes_allocated的原子操作fetch_add。这是一个重要的步骤,确保了内存申请的准确性。
而在GC过程中,当每个Space释放一定对象和内存后,会进一步同步到虚拟机的Heap对象,更新虚拟机整体的内存使用。这一过程的接口是Heap::RecordFree。这里需要注意的是,释放操作会同步更新num_bytes_allocated_的值,体现了内存管理的精细化。
基于这样的机制,我们有一个大胆的设想:在合适的时机,人为主动调用RecordFree接口,减去LargeObjectMapSpace管理的内存值。这样,Heap::num_bytes_allocated_统计的就将是其他内存Space的内存使用。换句话说,通过LargeObjectMapSpace申请的内存将会“脱离”虚拟机Heap::num_bytes_allocated_的统计,进入Native层的内存管理。这些对象的引用和内存回收机制仍然由虚拟机管理,从而避免了内存泄漏的风险。
接下来是流程示意部分。LargeObjectMapSpace申请的内存直接映射到虚拟内存。对于32位环境,应用空间可映射内存在3G左右,但虚拟机本身占用部分地址空间,应用侧实际使用范围在2G左右。极端情况下,调整后的虚拟机内存理论范围将在512M至2.5G之间。这部分的示意图可以清晰地展示这一过程。
我们转向关键实现部分。除了考虑接口代理问题外,还需要解决几个关键任务:如何实时获取当前Space已申请的内存大小?如何在合适时机同步Heap::num_bytes_allocated_的内存统计?如何“跳过”虚拟机在内存释放过程中的一致性校验问题?这些都是需要深入研究和解决的问题。在源码层面进行定制当然是最佳选择,但考虑到不同Android版本的兼容性,我们需要通过InlineHook技术代理相关接口,并在执行过程中调整相关参数以达到目的。
虚拟机内存管理是一个复杂而精细的过程。通过深入理解其工作机制,我们可以找到优化和改进的方法,以满足更广泛的应用需求。关于获取LargeObjectMapSpace当前内存大小的问题
对于第一个问题,尽管LargeObjectSpace提供了一个获取当前内存大小的接口,即LargeObjectSpace::GetBytesAllocated,但这个接口并未公开。在Android Q中,我们需要Libart.so库中的GetBytesAllocated符号来获取此功能。该符号的函数签名是_ZN3art2gc5space16LargeObjectSpace17GetBytesAllocatedEv。在实际运行中,我们需要动态获取这个符号在内存中的地址。
由于GetBytesAllocated是一个非静态函数,我们需要获取LargeObjectMapSpace的实例才能调用此接口。我们可以通过inlineHook代理“LargeObjectMapSpace::Alloc”来获取LargeObjectMapSpace的实例。LargeObjectMapSpace::Alloc的部分源码如下:
```scss
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的内存大小。
接下来是关于如何从虚拟机Heap中“移除”LargeObjectSpace实际占用的内存的问题。在能够实时获取LargeObjectSpace的内存使用情况后,我们可以通过"Heap::RecordFree"函数符号来实现这一目标。通过调用Heap::RecordFree的函数接口,我们可以增加或减少Heap中我们想要更新的内存大小。这个操作实质上是对内存管理的一个精细控制,确保系统的资源得到有效利用。
虚拟机内存管理的进阶之路:Heap与LargeObjectSpace的互动
在高级的内存管理策略中,Heap的灵活更新是关键。当我们掌握了适当的时机,就能够对LargeObjectSpace的内存进行“移除”操作,并实时更新Heap的内存容量。经过深入研究,我们发现这一过程的实现,需要在LargeObjectSpace的内存申请和回收过程中进行强制更新,从而使得虚拟机忽略对LargeObjectSpace的内存统计。
在LargeObject的申请过程中,一旦内存申请成功,我们首先在虚拟机内存统计中减去这部分内存。接着,虚拟机在返回实例化对象后,会统计本次新增的内存。通过这种“先减后加”的方式,我们保持了内存水位的稳定,从而间接实现了虚拟机“忽略”本次内存开销的效果。
在后续的GC过程中,如果部分或全部LargeObjectSpace中的对象被释放,正常情况下,释放的内存会同步到Heap中,以更新整体使用内存及可用内存。但我们已经将LargeObjectSpace的内存从Heap统计中移除,如果从Heap中减去这些释放的内存,会导致Heap统计的内存偏少。我们需要主动将这部分释放的内存“补偿”回来,以避免统计混乱。
这一改造的实现,使得在内存回收过程中对大对象内存的管理更加精细。改造后,Heap统计的内存将不再包含LargeObjectSpace管理的内存,从而间接扩大了其他内存Space的使用上限。对于LargeObjectSpace来说,虚拟机统计到的该内存Space一直为0,这意味着LargeObjectSpace内部并没有内存限制,异常情况下该Space的内存使用上限将显著提升。在Android O以下系统,这部分内存Space不仅涵盖Bitmap,还包括其他大对象。
在适配过程中,我们发现Android L版本之后的虚拟机会在GC过程中进行内存校验。如果当前使用内存加上释放内存小于GC之前的内存,虚拟机会抛出“断言”异常。为了确保这一过程的顺利进行,我们需要确保GC结束后,虚拟机已使用的内存加上本次GC释放的内存理论上要大于等于GC之前虚拟机使用的内存。如果不满足这一条件,系统将抛出严重的异常。我们在进行内存管理改造时,必须充分考虑这一校验机制,以确保系统的稳定运行。
在内存垃圾回收(GC)过程中,我们对Heap当前使用内存大小进行了动态调整。这一操作可能导致GC结束后所获得的Heap当前使用内存值小于实际值。为了确保校验逻辑的准确性,我们需要对Heap::GrowForUtilization接口进行代理操作。具体来说,我们将强制将bytes_allocated_before_gc参数设置为0,以保证校验始终成立。尽管这种调整看似微小,但从后续逻辑和实际测试来看,它对后续的内存GC并没有显著影响。
至此,我们已完成了对Android虚拟机内存统计策略的改造。这一方案不仅间接提升了虚拟机其他内存空间运行时的使用上限,还将LargeObjectSpace的内存使用上限完全脱离了虚拟机的限制,使其与Native内存属性等同管理。相较于Android系统的Bitmap内存管理改造,这一方案更为彻底,为应用的内存环境带来了极大的改善。
尽管我们在内存统计策略改造上取得了一定成果,但我们仍继续思考如何进一步优化。我们发现一期方案并非最优解,因为在应用运行过程中,大多数情况下并不会发生OOM。我们提出了一种新的思路:在监测到OOM时,再启动优化方案,以应对当前的OOM问题。这种智能化的按需开启方式被视为是一种极致化的解决方案。
针对上述思考,我们决定采用“OOM探测+按需开启”的策略来完成对内存的按需扩展。具体来说,我们将对内存申请过程进行定向监控,当监听到内存不足即将抛出OOM异常时,进行拦截,并激活mSponge方案一期的内存优化策略(即从Heap内存统计中移除当前LargeObjectSpace使用内存),然后再触发一次内存申请,以确保内存成功申请。
基于上述思路,我们需要在虚拟机内部监听并拦截OOM。当监听到第一次OOM时,我们主动将LargeObjectSpace的内存从Heap统计中移除,以增加空闲内存,并启动mSponge一期优化策略。这样,后续LargeObjectSpace的内存变化就不会影响Heap内存统计。这一二期的方案示意图清晰地展示了整个流程。
二期的技术实现主要涉及以下几个流程:监听并判断是否需要拦截OOM异常;监听内存分配结果;重试内存申请。我们代理Heap::ThrowOutOfMemoryError来监听并判断是否需要拦截OOM异常。如果可拦截,则代理Heap::AllocateInternalWithGc来监听本次内存申请过程中是否发生了OOM并被拦截。如果发生OOM并被拦截,我们通过AllocateInternalWithGc触发一次内存申请,以确保内存申请成功。在此过程中,我们禁止拦截本次内存申请过程中可能抛出的OOM。
我们的目标是通过对内存管理的精细化调整,提高应用的内存使用效率,减少OOM的发生,从而为应用的运行提供一个更加稳定和高效的环境。深入解读并重塑关于内存管理的内容