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 Size
to . 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
, Application
setting android:largeHeap="true"
and not setting largeHeap
the 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 space
and dalvik-main space 1
heap 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:largeHeap
,main 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 GC
是 SS GC
的改进版本,作为 background GC
的默认实现方式。
这两种 GC 的共同点,存在两片大小和堆大小一样的内存空间分别作为 From Space
和 To Space
,这两片区域分别为 dalvik-main space1
和 dalvik-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 Space
和To Space
的内存地址 - 调用
munmap
函数释放掉其中一个Space
所占用的内存
如何确定 From Space 和 To Space 的内存地址
我们需要读取 mpas 文件,然后搜索关键字 main space
和 main space 1
,就可以知道 main space
和 main 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);
}
复制代码
正如如源码所示,后台回收器类型 kCollectorTypeHomogeneousSpaceCompact
和 kCollectorTypeCC
才会创建 main space2
。
kCollectorTypeHomogeneousSpaceCompact
(同构空间压缩(HSC),用于后台回收器类型)kCollectorTypeCC
(Compacting GC
) 分为两种类型Semi-Space(SS)GC
(半空间压缩)Generational Semi-Space(GSS)GC
(分代半空间压缩),GSS GC
是SS 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、性能优化、系统源码、算法及数据结构、动画、大厂面经,真诚推荐你关注我。
- 公众号: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
-
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