微信黑科技

微信黑科技

  • 如果评论区没有及时回复,欢迎来公众号:ByteCode 咨询
  • 公众号:ByteCode。致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、算法、译文、系统源码相关的文章

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 源码的设置如下如下图所示

https://android.googlesource.com/platform/frameworks/native/+/master/build

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

上面的这两块区域对应的源码 地址

https://cs.android.com/android/_/android/platform/art/+/5f0b71ab2f60f76b5f73402bd1fdd25bbc179b6c:runtime/gc/heap.cc

执行 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,总来的来说分为两步:

https://github.com/Tencent/matrix/blob/master/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/GCSemiSpaceTrimmer.cpp

  • 确定 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 查看。


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


最新文章

开源新项目

  • 云同步编译工具(SyncKit),本地写代码,远程编译,欢迎前去查看 SyncKit

  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit

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

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

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,在技术的道路上一起前进

Android10 源码分析

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis

算法题库的归纳和总结

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin

精选国外的技术文章

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation

评论