Silk
Silk: Runtime-Guided Memory Management for Reducing Application Running Janks on Mobile Devices
Silk 主要针对 Android 中 ART 与 Linux Kernel 对“内存热度”的理解不一致问题。
Linux Kernel 只能在 page granularity 上执行 LRU 和 swap,而 Java/Kotlin 应用实际上以 object granularity 访问 ART heap。一个 4 KB page 中可能存在几十甚至上百个不同热度的对象,因此 page hotness 并不能准确代表 object hotness。另一方面,GC 对对象的扫描又会污染 Kernel LRU 所观察到的访问历史,使 GC-cold object 被误认为 hot、GC-hot object 被错误 swap out。论文将这两类现象统称为 Object Hotness Inversion。
Silk 因此设计两个互补组件:
MO-App
解决 Application Thread 的 Object Hotness Inversion
MO-GC
解决 GC Thread 的 Object Hotness Inversion
Architecture
+---------------------------------------------------------------------+
| Android Framework / ART |
| |
| [FG App Detector] |
| | |
| +----------------------+ |
| | |
| Application Thread | |
| | | |
| v v |
| [ART JIT Compiler] [GC Profiler] |
| | | |
| | sampled object access | FG/BG state |
| v | region type |
| [Object Access Time] | GC history |
| [stored in Lock Word] | |
| | v |
| v [GC Region Hotness] |
| [MO-App Hotness Recognition] | |
| | | |
| v | |
| [CC GC Copying Phase] | |
| | | |
| v | |
| [Group Objects by Hotness] | |
| | | |
| Hot/Warm/Cold Regions | |
+--------------------------------------------------|------------------+
|
Runtime GC semantics
|
+--------------------------------------------------v------------------+
| Linux Kernel |
| |
| [GC Hotness Metadata] |
| | |
| v |
| [kswapd / isolate_lru_pages()] |
| | |
| coldest GC regions first |
| | |
| v |
| [Page Reclaim] |
| | |
| v |
| [Swap Partition] |
| |
| if insufficient reclaim -> fallback to normal LRU |
+---------------------------------------------------------------------+
Silk 的核心其实是两种不同的 Runtime-Guided Memory Management:
MO-App:
Object Hotness
↓
改变 object physical layout
↓
让 page hotness 更接近 object hotness
↓
Kernel 原本的 LRU 就能做出更正确的判断
而:
MO-GC:
GC Semantics
↓
Region Hotness
↓
直接指导 Kernel reclaim page 的选择
所以 MO-App 主要改变对象布局,MO-GC 主要改变内核 reclaim 的 page-selection policy。这也是理解整个 Silk 最重要的一点。
MO-App
目标
MO-App,即:
Managing Objects with Hotness for Application Threads
用于解决 Application Thread 的 Object Hotness Inversion。
普通 Android 中:
Page A
+------------------------------------------------+
| Hot Object | Cold | Cold | Cold | Cold | Cold |
+------------------------------------------------+
Kernel 只能看到:
Page A recently accessed
↓
Page A = Hot
于是里面大量真正 cold 的对象也因为与一个 hot object 共处一页而被认为是 hot。
论文实验中,一个 ART page 往往包含大量对象,并且 hottest pages 中平均约 85.1% 的对象实际上属于 pseudo-hot objects。 Silk 希望最终形成:
Hot Page
+--------------------------------+
| Hot | Hot | Hot | Hot | Hot |
+--------------------------------+
Warm Page
+--------------------------------+
| Warm | Warm | Warm | Warm |
+--------------------------------+
Cold Page
+--------------------------------+
| Cold | Cold | Cold | Cold |
+--------------------------------+
从而让:
Page Hotness ≈ Object Hotness
1. Tracking Object Access
Silk 修改 ART 的 JIT Compiler,在编译应用代码时插入少量 tracking instructions。
例如:
obj1 = obj2.f;
或:
obj2.f = obj3;
访问的实际对象是:
obj2
因此 Compiler 可以在这类 object access 发生时记录 obj2 最近一次被访问的时间。
但是如果每一次 object access 都记录,CPU 开销会很大。
Silk 因此采用:
Sampling
+
Object Hotness Inheritance
论文的观察是:
Object A 被访问
↓
A 引用的对象
↓
很可能在之后也被访问
即对象访问存在一定的 hotness inheritance。
因此没有必要跟踪所有对象,只需要:
Sample Object
↓
记录 Hotness
↓
沿 reference chain
↓
推测附近对象的 Hotness
论文默认 tracking sampling interval 设置为 3。同时,在后续热度推断时,对没有直接采样到的对象,会检查 reference distance (k=3) 内已经记录过的对象。
2. 使用 Lock Word 记录 Access Time
如果为每个 Java object 单独增加一个字段,例如:
Object Header
+8 Bytes
由于 Android 中大量对象只有几十字节,会造成很高的额外内存开销。
Silk 发现绝大多数对象长期处于:
Unlocked
状态,而 ART 的 32-bit lock word 在该状态下存在大量未使用 bit。
因此直接复用 lock word:
31 28 27 16 15 0
+--------------------+-----------------+---------------------+
| State Flag | Accessed Flag | Last Access Time |
+--------------------+-----------------+---------------------+
4 bits 12 bits 16 bits
其中:
Accessed Flag = 0x000
↓
普通 Unlocked
Accessed Flag = 0xFFF
↓
Accessed State
低 16 bit 用于记录:
Object Last Access Time
因此不扩大 Object Header,论文称其为 zero additional memory overhead。
Lock 状态变化
Silk 还需要处理 lock word 本身原来的用途。
例如:
Accessed
↓ lock()
ThinLocked
进入 ThinLocked/FatLocked 时,原先保存的 access time 会被覆盖。
因此 unlock 时 Silk 会重新:
Object -> Accessed
+
Current Access Time
对于 GC relocation:
Original Lock Word
↓
save
↓
ForwardingAddress
↓
GC Copy Object
↓
restore
↓
Original Lock Word
所以 GC 搬迁对象之后,其 access information 仍可以保存。
3. Object Hotness Recognition
MO-App 根据:
Last Access Time
+
LRU-like Policy
判断 object hotness。
可以把它抽象理解为:
\[Age(o)=T_{now}-T_{access}(o)\]其中:
Age 小
↓
最近访问
↓
Hot
Age 大
↓
长期没有访问
↓
Cold
论文实验中将 object hotness 划分为 9 个 level:
Hot1
Hot2
Hot3
Warm1
Warm2
Warm3
Cold1
Cold2
Cold3
其本质仍然是依据最近访问时间的 LRU-style classification。 对于没有直接记录 access time 的对象:
Accessed
↓
直接使用记录的 Access Time
Unlocked
↓
寻找 reference distance <= 3
附近的 Accessed Object
↓
继承其中最新的 Access Time
ThinLocked / FatLocked
↓
认为 Hot
HashCode
↓
认为 Cold
ForwardingAddress 状态则在进入对象复制前完成 hotness 判断。
4. 借助 GC 实现 Object Grouping
知道 object hotness 之后还有一个问题:
怎么把相似热度的对象放在一起?
如果 Silk 自己主动移动 object:
Move Object
↓
修改所有引用
↓
可能触发 swap-in
↓
大量 Read / Write I/O
开销会非常大。
Silk 因此没有额外执行 object relocation,而是复用 ART Concurrent Copying GC 本身的 Copying Phase。
原本 CC GC 就要执行:
Victim Region
↓
copy live objects
↓
Free Region
↓
update references
Silk 将其变成:
Victim Region
↓
Recognize Object Hotness
↓
GC Copy
↓
choose destination region according to hotness
↓
Hot Object ──→ Hot Region
Warm Object ──→ Warm Region
Cold Object ──→ Cold Region
所以:
- 不需要额外把 swapped-out object 读回来;
- 不需要额外完成一次 reference fix-up;
- 利用了 GC 本来就必须进行的 object copying。
ART 的一个 Region 为:
\[256\text{ KB}\]也就是:
\[256\text{ KB}/4\text{ KB}=64\]个物理 page。
当相似 hotness 的对象被放入同一个 Region 后,其对应的物理 pages 中 object hotness 也会更加一致,从而减少 Object Hotness Inversion。 因此 MO-App 的核心链路就是:
Object Access
↓
Compiler Sampling
↓
Record Last Access Time
↓
Recognize Hotness
↓
GC Copying
↓
Group Similar-Hotness Objects
↓
Page Hotness ≈ Object Hotness
↓
Normal Kernel LRU becomes more accurate
MO-GC
目标
MO-GC:
Managing Objects with Hotness for GC Threads
解决另一种 completely different hotness:
Application Hotness
!=
GC Hotness
一个 object 即使 application thread 不访问它,也可能被 GC 经常扫描。
反过来,某对象刚刚因为 GC scan 被访问:
GC scans object
↓
Kernel sees memory access
↓
LRU says "Hot"
但这个 region 可能之后很长时间都不会再被 GC 扫描。
于是 Kernel 观察到的:
Page Access Recency
并不能代表真正的:
Future GC Access Probability
这就是 GC Thread Object Hotness Inversion。
1. GC Hotness Recognition
Silk 根据两个主要信息判断 GC hotness:
Application State
FG / BG
+
Region Type
New / Old
ART 的 Generational Concurrent Copying 中:
New Region
↓
Minor GC 和 Major GC 都经常扫描
↓
GC Hot
Old Region
↓
主要由 Major GC 选择性扫描
↓
GC Colder
同时:
Foreground App
↓
allocation 快
GC 频率高
↓
GC Hotter
Background App
↓
allocation 慢
GC 频率低
↓
GC Colder
最终 Silk 得到四级顺序:
\[FG-N > BG-N > FG-O > BG-O\]即:
GC Hot
↑
FG-New
BG-New
FG-Old
BG-Old
↓
GC Cold
其中,对于不同 Background Applications 的 BG-O,Silk 还会使用历史 GC collection frequency 进一步判断热度。
2. Region-Level Hotness Metadata
MO-GC 不需要像 MO-App 一样给每个 object 保存 GC hotness。
因为 GC 本身工作的基本单元就是:
Region = 256 KB
所以直接以 Region granularity 保存 metadata。
其结构可以理解为:
+------------------------------------------+
| PID | Region Hotness | Region ID List |
+------------------------------------------+
|6793 | Cold | 15, 207, ... |
+------------------------------------------+
Region 地址通过:
\[reg_{start} = regionspace_{start} + reg_{ino}\times reg_{len}\]其中:
\[reg_{len}=262144\text{ bytes}=256\text{ KB}\]以及:
\[reg_{end}=reg_{start}+reg_{len}\]来定位。 默认一个应用最多管理 4096 个 Region,因此每个应用的 GC-hotness metadata 大约:
\[4096\times4B=16KB\]10 个同时运行的应用大约只有:
\[160KB\approx0.16MB\]额外 metadata。
3. Runtime-Guided Kernel Reclaim
当内存不足时,Linux:
Memory Pressure
↓
kswapd
↓
isolate_lru_pages()
↓
select pages
↓
reclaim / swap out
原生 Linux 主要依据:
Page-level LRU
判断 page 是否值得 reclaim。
Silk 修改 isolate_lru_pages() 的 page-selection policy:
kswapd
↓
isolate_lru_pages()
↓
query Runtime GC hotness
↓
GC-Coldest Regions
↓
reclaim their pages first
↓
warmer regions
↓
...
也就是说:
BG-Old
↓
优先回收
FG-Old
↓
BG-New
↓
FG-New
↓
尽量保留
直到 memory pressure 得到缓解。
如果 GC-cold region 无法提供足够的 reclaimable memory:
Silk policy
↓
not enough pages
↓
fallback
↓
Native Kernel LRU
这里和 HMS 有一个非常重要的区别:
Silk 本身主要没有改变“什么时候发生 reclaim”;kswapd 仍然是在 memory pressure 下被触发。Silk重点改变的是“reclaim 哪些 page”。
模型
Silk 实际上存在两套不同的 Hotness Model。
Application Hotness Model
针对 application thread:
Last Access Time
|
v
LRU-style Classification
|
+-----------+-----------+
| | |
Hot Warm Cold
更进一步划分成 9 个等级:
Hot1 / Hot2 / Hot3
Warm1 / Warm2 / Warm3
Cold1 / Cold2 / Cold3
其中核心变量是:
\[T_{access}(o)\]因此可抽象成:
\[H_{app}(o) = f(T_{now}-T_{access}(o))\]需要强调:这个公式是对论文机制的抽象表达,不是论文给出的显式公式。论文实际实现使用基于 last access time 的 lightweight LRU threshold classification。
GC Hotness Model
GC Thread 使用的不是 access timestamp,而是 Runtime semantics:
\[H_{gc} = f( ApplicationState, RegionType, GCHistory )\]最基本的排序:
\[FG-N > BG-N > FG-O > BG-O\]所以 Silk 对同一块内存实际上维护两种不同意义上的 hotness:
Object
├── App Hotness
│ "Application thread 会不会很快访问?"
│
└── GC Hotness
"GC thread 会不会很快扫描?"
这一点非常重要。
一个 object 完全可能:
App-Cold
GC-Hot
或者:
App-Hot
GC-Cold
Silk 不试图用一个统一 hotness 指标描述二者,而是分别优化。
运行时控制流
Silk 的运行过程最好拆成两条路径理解。
Path 1:Application Thread
1. Application executes
Java / Kotlin Code
↓
DEX
↓
ART JIT Compiler
↓
Native ARM64
Silk 修改 compiler,使其可以在 object access 上插入轻量 tracking code。
2. Sample Object Access
App Thread
↓
Object Access
↓
Compiler Sampling
↓
record current access time
访问时间保存在 object lock word 的空闲 bit 中。
默认:
Sampling Interval = 3
论文测得这一配置下 object-access tracking 平均 CPU overhead 大约 1.2%。
3. GC Trigger
ART 正常触发 Concurrent Copying GC:
GC
↓
Mark
↓
Copy
↓
Reclaim
Silk 不额外遍历整个 heap 来识别 hotness。
4. Hotness Recognition
在 GC 即将 copy live object 时:
Object
↓
Read Lock Word
↓
Last Access Time
↓
LRU Hotness Recognition
如果对象没有直接采样:
Reference Neighborhood
↓
distance <= 3
↓
find Accessed Objects
↓
inherit newest access time
由于需要处理的 object 本来就被 GC 拉入内存,所以避免为了 hotness analysis 单独触发 swap-in。
5. GC Copy + Object Grouping
Live Object
↓
Hotness
↓
GC Copy
↓
Corresponding Region
例如:
Hot Objects
↓
Hot Region
Cold Objects
↓
Cold Region
最终:
Object Hotness Alignment
↓
Page Hotness Alignment
↓
Kernel LRU becomes more accurate
Path 2:GC Thread / Kernel
1. Runtime Collects GC Semantics
Android Framework
↓
FG App Detector
ART
↓
Region Type
New / Old
GC Profiler
↓
Historical GC Frequency
2. Calculate GC Region Hotness
FG-New > BG-New > FG-Old > BG-Old
并建立:
PID
↓
Region Hotness
↓
Region ID List
3. Memory Pressure
当系统进入 low-memory condition:
Memory Pressure
↓
kswapd
4. Runtime-Guided Page Selection
原生方式:
kswapd
↓
LRU
↓
Cold Page
Silk:
kswapd
↓
isolate_lru_pages()
↓
Runtime GC Hotness
↓
GC-Cold Region
↓
Pages in GC-Cold Region
↓
Reclaim First
5. Swap Out
GC-Cold Page
↓
Reclaim / Swap Out
从而尽可能把:
GC-Hot Pages
保留在 DRAM 中。
这样下一次 GC:
GC
↓
scan GC-Hot Region
↓
still resident
↓
less swap-in
↓
less CPU / memory-bandwidth contention
↓
less application jank
如果 runtime-guided reclaim 不够,则回退到默认 Kernel LRU。
完整控制流
Silk
|
+-------------+-------------+
| |
v v
MO-App MO-GC
| |
| |
Application Object Access FG/BG State
| Region Type
v GC History
JIT Compiler Sampling |
| v
v GC Hotness
Record Access Time |
in Lock Word v
| Region Metadata
v |
GC Trigger |
| |
v |
Recognize App Hotness |
| |
v |
GC Copying Phase |
| |
v |
Group Objects by Hotness |
| |
v |
Homogeneous-Hotness Pages |
| |
| Memory Pressure
| |
| v
| kswapd
| |
| v
| isolate_lru_pages()
| |
| runtime guidance
| |
| v
| GC-Cold Pages First
| |
+-------------+-------------+
|
v
Fewer Hot-Page Evictions
|
v
Less Swap-In
|
v
Fewer Janks
一句话理解 Silk
Silk 的本质是把 ART 中“对象级的真实访问语义”暴露给 page-level memory management:MO-App 通过 JIT 采样对象访问并借助 GC copying 将相似热度对象聚集到相同 page/region,使 page hotness 更接近 object hotness;MO-GC 则根据前后台状态、Region 新旧和 GC 历史推断 GC working-set hotness,并指导 Linux 优先回收 GC-cold pages,从而减少 hot object 被错误 swap out 后产生的 swap-in 和 UI jank。