WeChat black technology

hi everyone, this is DHL. Worked for Meituan, Kuaishou, and Xiaomi. Public account: ByteCode, focusing on useful and interesting hard-core original content, Kotlin, Jetpack, performance optimization, system source code, algorithm and data structure, and face experience of big factories.

I have always believed that technology is used to serve users and improve user experience, not like Pinduoduo , which writes malicious code, manipulates users’ mobile phones , and uses technology to do some bad things. Today’s article mainly shares how WeChat uses hackers Technology, reduce 512MB memory, reduce OOM and Native Crash to improve user experience.

In the previous article, who moved my memory, revealing the secret of 90% drop in OOM crashes , shared memory-related knowledge points, including heap, virtual memory, causes of OOM, and why insufficient virtual memory mainly occurs in 32-bit There are many reasons for the lack of virtual memory on the device , and what black technologies are currently available to help us reduce OOM. Interested friends can go to check, and start to refine each knowledge point from this article.

With the growth of business, the problem of insufficient virtual memory on 32-bit devices will become more and more prominent, especially for large-scale applications. In addition to business optimization, some black technology is needed to reduce as much memory as possible. Today's article mainly analyzes the "heap space halving" solution shared by WeChat, which can reduce up to 512MB of memory, thereby reducing OOM and Native Crash , before we start, we need to introduce the knowledge points related to the heap.

According to the explanation in the Android source code, the size of the Java heap should be set according RAM Sizeto . This is an empirical value, and the manufacturer can change it. If the phone is rooted, you can also change it yourself. The setting of the Google source code is shown in the figure below .
android.googlesource.com/platform/fr…

RAM (MB)-dalvik-heap. mk heapgrowthlimit (MB) heapsize (MB) needs to set android: largeHeap to 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
No matter how much RAM, the maximum heap limit so far is 512MB

As shown in the table above AndroidManifest.xml, Applicationsetting android:largeHeap="true"and not setting largeHeapthe upper limit of the maximum heap obtained in the file node is different.

<application
    android:largeHeap="true">

</application>
复制代码

Why is android:largeHeap turned off by default?

The Java heap is used to allocate objects created by Java / Kotlin, and is managed and recycled by the GC. When the GC recycles, the objects in the From Space are copied to the To Space. These two areas are dalvik-main spaceand dalvik-main space 1heap Same, as shown in the figure below.

In the figure, we only need to pay attention to size (virtual memory). If the upper limit of the Java heap is 512 MB, then dalvik-main space(512 MB)and dalvik-main space 1(512 MB)occupy a total of 1G of virtual memory.

如果堆的上限越大,那么 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 查看。


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


最新文章

开源新项目

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

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

  • The most complete and up-to-date practical projects of AndroidX Jetpack related components and related component principle analysis articles are gradually adding new members of Jetpack, and the warehouse is continuously updated. Welcome to check AndroidX-Jetpack-Practice

  • LeetCode / Jianzhi offer, including a variety of problem-solving ideas, time complexity, space complexity analysis, online reading

Guess you like

Origin juejin.im/post/7223930180867063864