一、概述
Chromium 在系统资源有限的嵌入式平台,常常遇到内存吃紧、性能恶化的问题,尤其体现在加载超大页面的时候。针对内存占用进行优化,是性能优化的关键。
二、Chromium 中的 Memory Reducing 方式
Chromium 对内存的处理主要集中在 RenderThreadImpl::OnMemoryPressure() 函数中;RenderThread 对象初始化的时候,会将 this 指针与该成员函数绑定,作为参数实例化一个 MemoryPressureListener 对象。
1 2 3 | memory_pressure_listener_.reset(new base::MemoryPressureListener( base::Bind(&RenderThreadImpl::OnMemoryPressure, base::Unretained(this)))); |
我们可以设计一个 MemoryMonitor 的类,不断地查询内存状态。当内存吃紧时,经由 RenderViewObserver 通知到 MemoryPressureListener 对象,便会回调 OnMemoryPressure() 这个函数,并传入内存压力等级:
1 2 | base::MemoryPressureListener::NotifyMemoryPressure( base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL); |
1. 对于 DiscardableMemory 的清理
DiscardableMemory(以下简称 DM)是 Chromium 中用来缓存大对象(LOB)的内存管理类型,主要用于缺少内存交换空间(Swap)的移动终端或嵌入式设备、以及没有使用空闲内存来提升用户体验的桌面设备上。
DM 提供了一种较为简单的方式,能够更好地响应系统内存压力信号并及时释放内存。DM 有两种状态:锁定状态和解锁状态。处于锁定状态的 DM 不能被回收,解锁 DM 能够让操作系统按需回收。
DM 的实现中,定义了使用等级(usage level)去量化实例被使用的频率。随着某个实例被频繁地使用,其使用等级会相应增加。DM 以页为最小分配单位,由于内存对齐,所分配的总内存通常会比所申请的要大。因此对于小对象的内存分配,DM 并不是很有效率。需要注意的是,DM 的实例是线程不安全的,在使用时要确保没有资源竞争。
1 2 3 4 5 6 7 | void RenderThreadImpl::ReleaseFreeMemory() { base::allocator::ReleaseFreeMemory(); discardable_shared_memory_manager()->ReleaseFreeMemory(); if (blink_platform_impl_) blink::decommitFreeableMemory(); } |
注意:由于 DM 的架构在 47 及以后的版本中有较大改动,以下的两种清理方式仅适用于 39 及以前的版本。
ReduceMemoryUsage 方法将每个 DM 实例的使用等级降低到基础等级及以下。当 DM 的平均使用等级较高时,过于频繁地调用这个方法会降低性能。但是如果调用的频率太低,又会导致内存膨胀,因此需要选择合适的频率来达到平衡。
1 | base::DiscardableMemory::ReduceMemoryUsage(); |
ReduceMemoryUsageUntilWithinLimit 这个静态方法可以用来减少内存占用直到减少到给定的字节额度,让内存压力降低到正常水平。
1 | base::internal::DiscardableMemoryEmulated::ReduceMemoryUsageUntilWithinLimit(4 * 1024 * 1024); |
Image Cache 的清理会直接影响到用户的使用体验。当滚动页面或切换 Tab 时会重新 decode 页面图片,造成视觉上的卡顿感。因此只在内存严重不足时,才会去清理 Image Cache,并且这个频率不能过高。
1 | blink::WebImageCache::clear(); |
3. V8 中的 Garbage Collection
1 2 | blink::mainThreadIsolate()->LowMemoryNotification(); /* --> void Heap::CollectAllAvailableGarbage(const char* gc_reason); */ |
注意到 CollectAllAvailableGarbage 这个方法没有返回值,也没有指定释放的内存大小,它不会去做全面的 GC(全面的 GC 会潜在地导致 V8 的任务线程卡住)。关于 V8 的内存回收机制有很多文章分析得很透彻,在此不做详述。
三、MemoryPressureListener 的工作原理
1 2 3 4 5 6 7 8 9 10 | // Example: void OnMemoryPressure(MemoryPressureLevel memory_pressure_level) { ... } // Start listening. MemoryPressureListener* my_listener = new MemoryPressureListener(base::Bind(&OnMemoryPressure)); ... // Stop listening. delete my_listener; |
MemoryPressureListener 中定义了三个内存压力等级:None 等级表示内存足够使用,通常不会用来通知;Mederate 等级会尝试释放非急需且方便释放的内存;Critical 等级代表内存状态极度危险。
在 Linux 平台,如果没有足够的内存被归还给系统,导致系统可用内存不足,最终会触发 Kernel 里的 OOM (Out of Memory) killer。OOM killer 会杀掉最占用内存的进程,以腾出内存给系统使用,不致于让系统立刻崩溃。因此在内存状态为 Critical 的时候需要尽可能释放掉所有可能的内存。
1 2 3 4 5 | enum MemoryPressureLevel { MEMORY_PRESSURE_LEVEL_NONE = -1, MEMORY_PRESSURE_LEVEL_MODERATE = 0, MEMORY_PRESSURE_LEVEL_CRITICAL = 2, }; |
通知 MemoryPresssureListener 内存状况:
1 2 | base::MemoryPressureListener::NotifyMemoryPressure( base::MemoryPressureListener::MEMORY_PRESSURE_CRITICAL); |
值得注意的是,即便在同一个线程里,当收到系统内存吃紧的通知后,回调函数也不能保证会被同步执行。
四、Memory Monitor 的实现方式和策略
1. 关于 meminfo 的介绍
1 2 3 4 5 6 7 8 9 | $ cat /proc/meminfo MemTotal: 32832432 kB #所有可用RAM大小(物理内存减去一些预留位和内核的二进制代码大小) MemFree: 1215044 kB #LowFree与HighFree的总和, 系统尚未使用的内存 MemAvailable: 29878076 kB #3.14内核新增, 内核使用特定的算法估算出来的, 并不十分精确 Buffers: 2335496 kB #用来给文件做缓冲大小, 现今基本不用作衡量标准 Cached: 19630108 kB #被高速缓冲存储器用的内存的大小 == [diskcache - SwapCache] SwapCached: 13648 kB #被高速缓冲存储器用的交换空间的大小, 已被交换出来, 仍被存放在swapfile中 Active: 12905476 kB #活跃使用中的缓冲或高速缓冲存储器页面文件的大小,除非非常必要否则不会被移作他用*/ Inactive: 10707164 kB #在不经常使用中的缓冲或高速缓冲存储器页面文件的大小, 可能被用于其他途径 |
随着时间的推移,系统的可用内存会越来越少,直观的体现为 MemFree 值不断减小,Cached 值会先增长后减少。对于启用了 Swap 的平台,系统会将内非活动内存换页到 Swap,以提高可用内存。
2. 对 RAM Cache 的清理
1 2 3 | echo 1 > /proc/sys/vm/drop_caches # 释放 page cache 中可释放的部分 echo 2 > /proc/sys/vm/drop_caches # 释放 dentries 和 inodes 缓存 echo 3 > /proc/sys/vm/drop_caches # 同时释放上述两项 |
如果 page cache 中有脏数据时,操作 drop_caches 是不能释放的,必须通过 sync 命令将脏数据刷新到磁盘,才能通过操作 drop_caches 释放 page cache。
注:/proc 目录是 Linux 上的一个虚拟文件系统,我们可以通过对它的读写来与 kernel 实体间进行通信,从而对当前 kernel 的行为做出调整。
发表回复