Android运行时堆组成
Anroid运行时堆可以分成2种,一种是ZygoteSpace区,一种是ApplicationSpace
- ZygoteSpace 区
- 初始化时创建:在启动Android系统时,由Zygote进程初始化并创建。
- 预加载核心类和资源:Zygote进程会加载所有核心类和资源,存储在ZygoteSpace中。
- 享元模式:ZygoteSpace的设计类似享元模式,通过共享来减少对象的创建次数,从而节省内存。
- 数据共享:ZygoteSpace中的数据是多个进程共享的。从Zygote进程派生出来的每个新进程都会共享这些预加载的数据。
ZygoteSpace区存放了哪些类还有资源
- 核心系统类
- Java核心库:如
java.lang.,java.util.
及其他标准的Java核心库。 - Android框架类:如
android.app.,android.content.,android.view.*
等用于应用开发的基本框架类。
- 关键组件和服务
- Activity及其生命周期:Activity 类及相关生命周期管理类。
- Content Providers:管理应用程序间数据共享的内容提供者类。
- Broadcast Receivers:处理广播消息的接收器类。
- Services:后台服务类及其管理机制。
- 常用资源
- 字符串资源:系统级别常用的字符串和文本资源。
- Drawable资源:系统默认的可绘制资源,如图标、按钮背景等。
- 常用静态库和依赖
- 基础静态链接库:如C++标准库和其他系统级别的基础静态库。
- 其他第三方依赖:系统常用的第三方库预加载。
- VM(虚拟机)初始化配置
- Dalvik/ART配置:虚拟机相关的配置信息、优化参数等。
为什么要预加载这些内容?
提升应用启动速度
- 通过在系统启动时预加载这些常用类和资源,避免了每次应用启动时的加载过程,显著提升了应用启动速度。
内存共享和节省 - 使用写时复制技术(Copy-on-Write),多个进程共享同一块内存,只有当某个进程需要修改这些共享内容时,才会复制一份新的副本。这种机制极大地节省了内存开销。
- ApplicationSpace 区
- 应用程序启动时创建:每当启动一个新应用程序时,会为其创建一个新的ApplicationSpace。
- 动态对象创建:新创建的对象存放在ApplicationSpace中。
- 数据独立:ApplicationSpace中的数据不是进程间共享的。不同的进程拥有各自独立的ApplicationSpace。
- 一个进程对应一个ApplicationSpace:每个应用进程有独立的ApplicationSpace,进程内部的数据自成一体,不与其他进程共享。
图示理解
±---------------------------+
| ZygoteSpace |
| (共享核心类和资源) |
±---------------------------+
| |
±-----------+ ±------------+
| |
±--------v---------+ ±--------v---------+
| 应用进程1 (App1) | | 应用进程2 (App2) |
| ±-------------+ | | ±-------------+ |
| |ApplicationSpace| | | |ApplicationSpace| |
| | (新创建对象) | | | | (新创建对象) | |
| ±-------------+ | | ±-------------+ |
±-------------------+ ±-------------------+
结论
- ZygoteSpace:
- 在系统启动时由Zygote进程创建,用于存储预加载的核心类和资源。
- 使用享元模式,通过共享减少对象的创建次数,节省内存。
- 这些数据在多个进程之间共享。
- ApplicationSpace:
- 在应用程序启动时创建,用于存放新创建的对象。
- 数据独立,每个进程有独立的ApplicationSpace。
- 一个进程对应一个ApplicationSpace,每个应用进程的数据互不干扰、不共享。
最后总结 - Android运行时堆确实被划分为ZygoteSpace和ApplicationSpace两部分。
- ZygoteSpace通过共享预加载的核心类和资源在多个进程中实现内存节省。
- ApplicationSpace则确保了每个进程的数据独立,不同进程有各自的ApplicationSpace。
毕竟这是个堆,用来存放对象的,所以肯定要涉及到垃圾回收,Android运行时堆用的是标记-清除法(我估计这个是早期版本,现在新的版本还用标记-清除法就效率太低了)
所以里面还存放和GC有关的东西
3.Card Table
用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息
4.Heap Bitmap
有2个Heap Bitmap,一个用来记录上次GC回收存活的对象,一个用来存放这次GC回收存活的对象
5.Mark Stack
DVM的运行时堆使用标记-清除算法进行GC,Mark Stack就是在GC的标记阶段使用,它用来遍历存活的对象
最开始没理解Heap Bitmap和Mark Stack
因为用的标记-清除法
在标记的时候,Mark Stack会将在可达性链路上的对象标记为1,表示不应该被回收,将不在可达性链路上的对象标记为0,表示应该被回收的对象,然后Mark Stack存储的是在可达性链路上的对象
然后在清除的时候,会把Heap Bitmap为0的清除掉
然后我再想,那么我们为什么要有Mark Stack?直接Heap Bitmap不可以吗
然后看了一下,
1:就是Heap Bitmap其实默认都是0的,我们将在可达性链路上的对象标记为1之后,存放在Mark Stack里面之后,后面会把Mark Stack里面的对象弹出来然后将Heap Bitmap里面对应的对象设置为1
2:如果我们不使用这个Mark Stack显式堆栈的话,那么我们进行标记的时候,用递归的方式标记,如果链路太长(因为我们每次方法的调用都是一次栈帧的入栈),那么这个虚拟机栈就会装入太多栈帧,它的长度受到了操作系统的限制,如果太多,可能就会导致一个栈的溢出。
但是如果我们选择用这个Mark Stack的话,就可以用迭代的方式而避免用递归
//递归实现
void markObject(Object obj) {
if (obj == null || obj.isMarked()){
return;
}
obj.mark();
for (Object ref : obj.getReferences()) {
markObject(ref);
}
}
//显式堆栈实现
void markObject(Object root) {
Stack markStack = new Stack<>():
markStack.push(root);
while (!markStack.isEmpty()) {Object object object pop();if (obj == null || obj.isMarked()) {continue;}obj.mark();for (Object ref : obj.getReferences()) {if (ref != null && !ref.isMarked()) {markStack.push(ref);}}}
}
DVM的GC日志
DVM中每次垃圾收集都会将GC日志打印到logcat中
具体格式为
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>,<Pause_time>
GC_Reason
这个就表示的GC的理由
一般是4个理由,
第一个
GC_FOR_ALLOC
就是我们的堆已经满了,但是此时我们又创建了新的对象,这个对象已经放不进去堆了,就得先进行一个GC回收,然后才能放进去
这种 GC 是一种“阻塞”GC,意味着应用程序的所有线程会暂停(Stop-the-world),直到 GC 完成。这是强制性的垃圾回收,用于在没有足够内存分配新对象时,腾出空间。
影响:这种 GC 会对应用性能产生影响,因为在 GC 运行时,应用程序会被暂停。
第二个
GC_EXPLICIT
就是我们手动GC,比如调用
System.gc()
这是开发者或系统明确发起的 GC。在开发过程中,开发者可能希望强制进行垃圾回收以便释放内存,但是否真正执行完全取决于虚拟机。即便调用了 System.gc(),虚拟机也可能根据当前内存情况决定是否进行实际的回收。
影响:可能导致应用线程暂停,具体取决于内存情况和 GC 类型。通常不建议频繁调用 System.gc(),因为可能会增加不必要的内存回收开销
第三个
GC_HPROF_DUMP_HEAP
就是比如我们使用leakcanary来检测内存泄露,这个时候会创建一个HPROF文件,这个时候就会启动一个GC
为了确保内存快照的准确性,GC 会在生成 HPROF 文件之前进行一次完全的垃圾回收,以便从堆中回收所有不再使用的对象。
影响:GC_HPROF_DUMP_HEAP 会暂停应用程序,确保内存快照的准确性。在生成大文件时可能会造成较长时间的暂停。
第四个
GC_CONCURRENT
当堆内存即将填满时,虚拟机会尝试进行并发垃圾回收。这是为了在堆接近满时,主动回收内存,避免频繁触发 GC_FOR_ALLOC。
并发 GC 允许应用程序继续运行,同时在后台执行垃圾回收。应用线程在 GC 标记阶段的某些时刻可能会短暂地暂停,但整体上对应用程序的影响较小。
影响:相较于 GC_FOR_ALLOC,GC_CONCURRENT 影响较小,因为它是并发执行的,减少了应用程序的停顿时间。
Amount_freed
表示此时GC释放内存的大小
Heap_stats
表示堆的空闲内存百分比
External_memory_stats
现在高版本的已经不用了,懒得看了
Pause_time
我们进行GC的时候,会让线程暂停一下,这个Pause_time就表示暂停的时间