hi 大家好,我是 DHL。就职于美团、快手、小米。公众号:ByteCode,专心有用、有趣的硬核原创内容,Kotlin、Jetpack、功能优化、体系源码、算法及数据结构、大厂面经。

我一向以为技能是用来服务于用户,提高用户体验,而不是像 拼多多,写了恶意代码,操控用户的手机 ,运用技能做一些欠好的事,今日这篇文章首要共享微信怎么运用黑科技,削减 512MB 内存,下降 OOM 和 Native Crash 提高用户体验。

在上一篇文章 谁动了我的内存,揭秘 OOM 溃散下降 90% 的隐秘 中共享了内存相关的知识点,包含堆、虚拟内存、产生 OOM 的原因,以及 为什么虚拟内存不足首要产生在 32 位的设备上导致虚拟内存不足的原因都有那些,现在都有哪些黑科技协助咱们去下降 OOM,有兴趣的小伙伴能够前往检查,从这篇文章开端细化每个知识点。

跟着业务的增加,32 位设备上虚拟内存不足问题会越来越突出,尤其是大型应用会愈加显着。除了业务上的优化之后,还需求一些黑科技尽可能下降更多的内存,而今日这篇首要剖析微信共享的「堆空间减半」的计划,最高可削减 512MB 内存,从而下降 OOM 和 Native Crash,在开端之前,咱们需求介绍一下 相关的知识点。

依据 Android 源码中的解释,Java 堆的巨细应该是依据 RAM Size 来设置的,这是一个经验值,厂商是能够更改的,假如手机 Root 之后,自己也能够改,Google 源码的设置如下如下图所示。
android.googlesource.com/platform/fr…

微信黑科技

RAM (MB)-dalvik-heap. mk heapgrowthlimit (MB) heapsize (MB) 需求设置 android: largeHeap 为 true
512-dalvik-heap. mk 48 128
1024-dalvik-heap. mk 96 256
2048-dalvik-heap. mk 192 512
4096-dalvik-heap. mk 192 512
6144-dalvik-heap. mk 256 512
不管 RAM 多大,到现在为止堆的最大上限都是 512MB

正如上面表格所示,在 AndroidManifest.xml 文件 Application 节点中设置 android:largeHeap="true" 和不设置 largeHeap 获取到的最大堆的上限是不相同。

<application
    android:largeHeap="true">
</application>

为什么默许封闭 android:largeHeap

Java 堆用于分配 Java / Kotlin 创立的目标,由 GC 办理和收回,GC 收回时将 From Space 里的目标仿制到 To Space,这两片区域别离为 dalvik-main spacedalvik-main space 1, 这两片区域的巨细和 Java 堆巨细相同,如下图所示。

微信黑科技

图中咱们只需求重视 size(虚拟内存) 即可,假如 Java 堆的上限是 512 MB,那么 dalvik-main space(512 MB)dalvik-main space 1(512 MB) 共占用 1G 的虚拟内存。

假如堆的上限越大,那么 main space 占用的虚拟内存就会越大,在 32 位设备上,用户空间可用虚拟内存只要 3G,可是假如堆上限是 512MB,那么 main space 一共占用 1G 虚拟内存,剩余只要 2G 可用,因而 Google 在默许情况下会封闭 android:largeHeap 选项,只要在有需求的时分,自动设置 android:largeHeap = true,测验获取更大的堆内存。

main space 占用虚拟内存的核算办法是不相同的。

Android 5. x ~ Android 7. x

  • 假如设置 android:largeHeap = true 时,main space size = dalvik.vm.heapsize,假如 heapsize 是 512MB,那么两个 main space 共占用 1G 虚拟内存
  • 假如不设置 largeHeap,那么 main space size = dalvik.vm.heapgrowthlimit,假如 heapgrowthlimit 是 256 MB,那么两个 main space 共占用 512 MB 虚拟内存

>= Android 8. x

不管 AndroidManifest 是否设置 android:largeHeapmain space size = dalvik.vm.heapsize * 2,假如 dalvik.vm.heapsize 是 512MB 那么 main space 占用 1G 的虚拟内存内存。

main space 在不同的体系分配办法是不相同的。

  • Android 5.x ~ Android 7.x 中,体系分配两块 main space,它们占用虚拟内存的巨细和堆的巨细是相同的
  • >= Android 8.x 之后,只分配了一个 main space,可是它占用虚拟内存的巨细是堆的 2 倍

不同的体系上,它们的完成办法是不相同的,所以咱们要选用不同的办法来开释 main space 占用的内存。

在 Android 5. x ~ Android 7. x

5.0 之后运用的是 ART 虚拟机,在 ART 虚拟机引入了,两种 Compacting GC 分为 Semi-Space(SS)GC (半空间紧缩) 和 Generational Semi-Space(GSS)GC (分代半空间紧缩)。 GSS GCSS GC 的改善版本,作为 background GC 的默许完成办法。

这两种 GC 的共同点,存在两片巨细和堆巨细相同的内存空间别离作为 From SpaceTo Space,这两片区域别离为 dalvik-main space1dalvik-main space2

微信黑科技

上面的这两块区域对应的源码 地址。
cs.android.com/android/_/a…

微信黑科技

履行 Compact / Moving GC 的时分才会运用到这两片区域,在 GC 履行期间,将 From Space 分配的还存活的目标会顺次拷贝到 To Space 中,在仿制目标的过程中 From Space 中的碎片就会被消除,下次 GC 时重复这套逻辑,可是 GSS GC 还多了一个 Promote Space

Promote Space 首要存储老时代的目标,老时代目标的存活性要比新生代的久,因而将它们拷贝到 Promote Space 中去,能够避免每次履行 GSS GC 时,都需求对它们进行无用的处理。

新生代和老时代选用的不同的算法:

  • 新生代:仿制算法。在两块 space 来回移动,高效且履行频频,每次 GC 不需求挂起线程
  • 老时代:标记-紧缩算法。会在 Mark 阶段是在挂起除当时线程之外的所有其它运行时线程,然后在 Compact 阶段才移动目标,Compact 办法是 Sliding Compaction,也便是在 Mark 之后就能够按次序一个个目标 “滑动” 到空间的某一侧,移动的时分都是在一个空间内移动,不需求多一份空间

怎么开释掉其间一个 main space 占用的内存

开释计划,能够参阅腾讯开源的计划 Matrix,总来的来说分为两步:
github.com/Tencent/mat…

  • 确定 From SpaceTo Space 的内存地址
  • 调用 munmap 函数开释掉其间一个 Space 所占用的内存

怎么确定 From Space 和 To Space 的内存地址

咱们需求读取 mpas 文件,然后查找关键字 main spacemain space 1,就能够知道 main spacemain space 1 的内存地址。

当咱们知道 space 的内存地址之后,咱们还需求确认当时正在运用的是那个 space,才能安全的调用 munmap 函数,开释掉别的一个没有运用的 space

matrix 的计划,创立一个根本类型的数组,然后经过 GetPrimitiveArrayCritical 办法获取它的地址,代码如下:

微信黑科技

调用 GetPrimitiveArrayCritical 办法会返回目标的内存地址,假如地址在那块区域,当时的区域便是咱们正在运用的区域,然后咱们就能够安全的开释掉别的一个 space 了。

微信黑科技

开释掉其间一个 Space 会有问题吗?

假如咱们直接开释掉其间一个 Space,在履行 Compact / Moving GC 的时分,需求将 From Space 分配的目标顺次拷贝到 To Space 中,因为找不到 To Space,会引起 crash, 所以需求阻挠 Moving GC

源码中也说明晰调用 GetPrimitiveArrayCritical 办法能够阻挠 Moving GC。

微信黑科技

GetPrimitiveArrayCritical 办法会调用 IncrementDisableMovingGC 办法阻挠 Moving GC,对应的源码如下。
https://android. googlesource. com/platform/art/+/master/runtime/gc/heap. cc #956

void Heap::IncrementDisableMovingGC(Thread* self) {
  // Need to do this holding the lock to prevent races where the GC is about to run / running when
  // we attempt to disable it.
  ScopedThreadStateChange tsc(self, kWaitingForGcToComplete);
  MutexLock mu(self, *gc_complete_lock_);
  ++disable_moving_gc_count_;
  if (IsMovingGc(collector_type_running_)) {
    WaitForGcToCompleteLocked(kGcCauseDisableMovingGc, self);
  }
}

所以只需求调用 GetPrimitiveArrayCritical 办法,阻挠 Moving GC,也就不需求用到别的一个空间了,因而能够安全的开释掉。

阻挠 Compact / Moving GC 会有功能问题吗

按照微信给出的测试数据,在功能上没有显着的变化。

微信黑科技

OS Version >= Android 8. x

8.0 引入了 Concurrent Copying GC(并发仿制算法),堆空间也变成了 RegionSpace。RegionSpace 的算法并不是靠把已分配目标在两片空间之间来回倒腾来完成的,剖析 smaps 文件,发现也只创立了一个 main space,可是它占用的虚拟内存是堆的 2 倍,所以 8.0 之前的计划开释别的一个 space 是无法运用的。

为什么没有创立 main space2

咱们从源码看一下创立 main space2 的触发条件。

if (foreground_collector_type_ == kCollectorTypeCC) {
    use_homogeneous_space_compaction_for_oom_ = false;
}
bool support_homogeneous_space_compaction =
  background_collector_type_ == gc::kCollectorTypeHomogeneousSpaceCompact ||
  use_homogeneous_space_compaction_for_oom_;
if (support_homogeneous_space_compaction ||
  background_collector_type_ == kCollectorTypeSS ||
  foreground_collector_type_ == kCollectorTypeSS) {
    ScopedTrace trace2("Create main mem map 2");
    main_mem_map_2 = MapAnonymousPreferredAddress(
        kMemMapSpaceName[1], main_mem_map_1.End(), capacity_, &error_str);
}

正如如源码所示,后台收回器类型 kCollectorTypeHomogeneousSpaceCompactkCollectorTypeCC 才会创立 main space2

  • kCollectorTypeHomogeneousSpaceCompact(同构空间紧缩(HSC),用于后台收回器类型)
  • kCollectorTypeCCCompacting GC) 分为两种类型
    • Semi-Space(SS)GC (半空间紧缩)
    • Generational Semi-Space(GSS)GC (分代半空间紧缩),GSS GCSS GC 的改善版本

而 Android 8.0 将 Concurrent Copying GC 作为默许办法,对应的收回器的类型是 kCollectorTypeCCBackground

微信黑科技

Concurrent Copying GC 分为 Pause, Copying, Reclaim 三个阶段,以 Region 为单位进行 GC,巨细为 256 KB。

  • pause: 这个阶段耗时非常少,这儿很重要的一块儿作业是确定需求进行 GC 的 region, 被选中的 region 称为 source region
  • Copying:这个阶段是整个 GC 中耗时最长的阶段。经过将 source region 中目标依据 root set 核算并标记为 reachable,然后将标记为 reachable 的目标拷贝到 destination region
  • Reclaim:在经过 Copying 阶段后,整个进程中就不再存在指向 source regions 的引用了,GC 就能够将这些 source region 的内存开释供今后运用了。

Concurrent Copying GC 运用了 read barrier 技能,来保证其它线程不会读到指向 source region 的目标,所以不会将 app 线程挂起,也不会阻挠内存分配。

怎么削减 main space 占用的内存

Adnroid 8.0 之后运用的阿里巴巴 Patrons 的计划,在虚拟内存占用超过一定阈值时调用 RegionSpace 中的 ClampGrowthLimit 办法来减缩 RegionSpace 的巨细。

可是 ClampGrowthLimit 只在 Android 9.0 今后才出现,8.0 是没有的,所以参阅了 Android 9.0 的代码完成了一个 ClampGrowthLimit。

微信黑科技

在 ClampGrowthLimit 办法中,经过调用 MemMap::SetSize 办法来调整 RegionSpace 的巨细。
https://android. googlesource. com/platform/art/+/5f0b71ab2f60f76b5f73402bd1fdd25bbc179b6c/runtime/gc/space/region_space. cc #416

微信黑科技

MemMap::SetSize 办法的完成。
https://android. googlesource. com/platform/art/+/android-9.0.0_r7/runtime/mem_map. cc #883

微信黑科技

new_base_size_base_size_ 不相等的情况下会履行 munmap 函数 , munmap 开释的巨细为 base_size_new_base_size_ 的差值。


全文到这儿就完毕了,感谢你的阅览,坚持原创不易,欢迎在看、点赞、共享给身边的小伙伴,我会持续共享原创干货!!!


我开了一个云同步编译东西(SyncKit),首要用于本地写代码,同步到长途设备,在长途设备上进行编译,最终将编译的结果同步到本地,代码已经上传到 Github,欢迎前往库房 hi-dhl/SyncKit 检查。

  • 库房 SyncKit:https://github.com/hi-dhl/SyncKit
  • 下载地址:https://github.com/hi-dhl/SyncKit/releases

Hi 大家好,我是 DHL,就职于 美团、快手、小米。公众号:ByteCode ,共享有用、有趣的硬核原创内容,Kotlin、Jetpack、功能优化、体系源码、算法及数据结构、动画、大厂面经,真挚引荐你重视我。

  • 公众号:ByteCode
  • 哔哩哔哩: space.bilibili.com/498153238
  • : juejin.im/user/259450…
  • 博客: hi-dhl.com
  • Github: github.com/hi-dhl

最新文章

  • 国外大厂面试题, 7 个 Android Lifecycle 重要的知识点
  • Android 13这些权限废弃,你的应用受影响了吗?
  • Android 12 已来,你的 App 溃散了吗?
  • Android 利器,我开发了云同步编译东西
  • Twitter 上有趣的代码
  • 谁动了我的内存,揭秘 OOM 溃散下降 90% 的隐秘
  • 反射技巧让你的功能提高 N 倍
  • 90%人不明白的泛型局限性,泛型擦除,星投影
  • 揭秘反射真的很耗时吗,射 10 万次耗时多久
  • Google 宣布废弃 LiveData.observe 办法
  • 影响功能的 Kotlin 代码(一)
  • 揭秘 Kotlin 中的 == 和 ===

开源新项目

  • 云同步编译东西(SyncKit),本地写代码,长途编译,欢迎前去检查 SyncKit

  • KtKit 细巧而实用,用 Kotlin 语言编写的东西库,欢迎前去检查 KtKit

  • 最全、最新的 AndroidX Jetpack 相关组件的实战项目以及相关组件原理剖析文章,正在逐渐增加 Jetpack 新成员,库房持续更新,欢迎前去检查 AndroidX-Jetpack-Practice

  • LeetCode / 剑指 offer,包含多种解题思路、时间复杂度、空间复杂度剖析,在线阅览