这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天,主要是对于自动内存管理和GC进行了深入学习。
性能优化层面:
业务代码--->SDK---->基础库---->语言运行时----->OS
-
业务层优化:
针对特定场景,具体问题和分析
容易获得较大性能收益
-
语言运行时优化
解决更通用的性能问题
考虑更多场景
Tradeoffs
自动内存管理
1.动态内存:程序在运行时根据需求动态分配的内存:malloc()
2.自动内存管理(垃圾回收):由程序语言的运行时系统回收动态内存
3.三个任务:
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
The GC runs concurrently with mutator threads
Mutator ------业务线程,分配新对象,修改对象指向关系
Collector:GC线程,找到存活对象,回收死亡对象的内存空间(collectors必须感知对象指向关系的改变)
Serial GC:只有一个collector
Parallel GC:支持多个collectors同时回收的GC算法
Concurrent GC:mutator(s)和collector(s)可以同时执行
评价GC算法:
- 安全性:不能回收存活的对象那个(基本要求)
- 吞吐率:1-(GC时间/程序执行时间)(花在GC上的时间)
- 暂停时间(业务是否感知)
- 内存开销(GC元数据开销)
推荐资料阅读:THE GARBAGE COLLECTION HANDBOOK
追踪垃圾回收:
对象被回收的条件---指针指向关系不可达的对象
- 标记根对象:静态变量、全局变量、常量、线程栈(标记根对象可活)
- 标记:找到可达对象(从根对象出发,找到所有可达对象)
- 清理:所有不可达对象
方法实现:-----------根据对象的生命周期,使用不同的标记和清理策略
Copying GC:将存活对象复制到另外的内存空间
Mark-sweep GC:将死亡对象的内存标记为“可分配”
Mark-compact GC:移动并整理存活对象
分代GC:
每个对象的年龄:经历过GC的次数
目的:针对年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销
不同年龄的对象处于heap的不同区域
年轻代:常规的对象分配,GC吞吐率高,由于存活对象少,可以采用Copying GC
老年代:对象趋向于一直活着,反复复制开销较大,可以采用mark-sweep collection
GC的主要算法:
1.引用计数:
每个对象都有一个与之关联的引用数目
对象存活的条件:当且仅当引用数大于0
缺点:
- 维护引用计数的开销比较大,通过原子操作保证对引用计数操作的原子性和可见性
- 无法回收环形数据结构(可以使用weak reference)
- 内存开销:每个对象都引入的额外内存空间存储引用数目
- 回收内存时依然可能引发暂停
优点:
- 内存管理的操作被平摊到程序运行中(指针传递过程中进行引用计数的增减)
- 不需要runtime细节,不需要标记GC roots
2.复制算法(新生代)
年轻代-----Minor GC
实现原理:Minor GC把Eden中所有活的存活对象都一道Survivor区域中,如果Survivor区中放不下,则剩下的活的对象就会被移到老年代,即一旦收集后,Eden就是空的。
缺点:
- 需要双倍空间,浪费了一般内存
优点:
- 复制过去都是挨在一起,没有内存碎片,并且复制时,新生区存活率比较低,切新生区内存也比较小。
3.标记清除(老年代)
老年代一般是由标记清除或标记清楚与标记整理的混合实现。
缺点:
- 扫描两次,耗时严重,效率比复制算法高
- 清楚会产生内存碎片
优点:
- 不浪费内存
4.标记压缩(老年代)
标记的存活对象会被整理,按照内存地址依次排列,而未被标记的内存会被清理。
缺点:
- 标记/整理算法效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。(效率低于复制算法)
优点:
- 弥补标记/清楚算法中,内存区域分散的缺点,消除了复制算法中,内存减半的高额代价。
5.标记清除压缩
先进行标记清楚,如果碎片多了,则再进行一次标记压缩。结合两个优点,减少移动对象的成本。