自动内存管理 | 青训营笔记

90 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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.标记清除(老年代)

老年代一般是由标记清除或标记清楚与标记整理的混合实现。

image.png

缺点:

  • 扫描两次,耗时严重,效率比复制算法高
  • 清楚会产生内存碎片

优点:

  • 不浪费内存
4.标记压缩(老年代)

标记的存活对象会被整理,按照内存地址依次排列,而未被标记的内存会被清理。

缺点:

  • 标记/整理算法效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。(效率低于复制算法)

优点:

  • 弥补标记/清楚算法中,内存区域分散的缺点,消除了复制算法中,内存减半的高额代价。

image.png

5.标记清除压缩

先进行标记清楚,如果碎片多了,则再进行一次标记压缩。结合两个优点,减少移动对象的成本。