这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
如果有错误和其他意见,麻烦留言指正💖
本文结合课堂内容和《深入理解Java虚拟机》整理了自动内存管理的基础知识。
前言
自动内存管理概念
自动内存管理是语言运行时在托管执行过程中提供的服务之一。 语言运行时的垃圾回收器为应用程序管理动态内存的分配和释放。动态内存是程序在运行时按需动态分配的内存,如c语言中的malloc函数申请内存。
作用
对开发人员而言,这就意味着在开发托管应用程序时不必编写执行内存管理任务的代码,专注于实现业务代码。 自动内存管理可解决常见问题,可以在很大程度上避免内存使用不当引发的正确性和安全性问题,例如,忘记释放对象并导致内存泄漏,或尝试访问已释放对象的内存(used-after-free problem),或者重复释放了同一块内存(double-free problem)。
任务
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
收集器分类
GC 算法一般会涉及到用户线程和GC线程。
- Mutator :用户线程或者叫业务线程。相当于程序员的业务代码,会在执行的过程中会不断分配新对象,修改对象指向关系。
- Colector:GC 线程,一般来说对程序员透明,自动工作,它的任务是找到存活对象,回收死亡对象的内存空间。
Serial GC
单线程收集器。这里的单线程不仅仅说明它只会使用一个CPU或一条收集线程取完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop the World),直到它收集结束。
也就是用户线程需要暂停来等待垃圾收集。可以想象,如果垃圾收集耗时较长,会阻塞用户线程,影响用户体验或者对业务造成滞后影响。
如上图,只有一个collector在触发 GC 后执行,并且暂停用户线程。
Parallel GC
多线程收集器,相当于Serial GC的多线程升级版,支持多个collectors 同时回收的GC算法
Concurrent GC
mutator(s) 和 collector(s) 可以同时执行,因此它必须感知对象指向关系的改变。
上图是CMS(Cocurrent Mark Sweep)收集器的工作图的例子。
GC算法
自动内存管理技术从大类上分为追踪垃圾回收和引用计数。
追踪垃圾回收
对象被回收的条件:指针指向关系不可达的对象
过程
-
枚举根节点
- 可作为根节点的节点主要在全局性的引用,如静态变量、全局变量、常量、线程栈中的本地变量表等。
-
标记:所有可达对象
- 求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
-
清理:所有不可达对象
不同算法对标记和清理有不同策略,常见算法由复制算法(Copying GC),标记-清除算法(Mark-sweep GC),标记整理算法(Mark-compaact GC)。我们要根据对象的生命周期,选择合适的算法。
标记-清除算法
标记-清除算法是最基础的收集算法。通过可达性分析标记存活对象和可回收对象。在标记完成后统一回收所有被标记的对象。 缺点:
- 标记和清除的效率都不高。
- 会产生大量不连续的内存碎片。
复制算法
复制算法将内存均分为两块区域。每次只使用其中一块,当这块内存用完了,就将还存活着的对象复制到另外一块上面,再把已使用过的内存空间一次清理掉。
由于每次都是对整个半区进行内存回收,内存分配时就不用考虑内存碎片的复杂情况,按顺序分配内存即可。
缺点:
- 有效内存减半,代价高
标记-整理算法
标记过程仍然和标记-清理算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后直接清理掉端边界以外的内存
分代GC
分代 GC 基于分代假说(Generational hypothesis),即绝大多数对象都是朝生夕灭的。
基于这个特定,根据对象存活周期的不同将内存划分为不同的区域,一般划分为老年代和新生代,将不同年龄的对象置于相应区域,然后根据各个年代的特点采用最适当的收集算法。
- 年轻代:存活对象少,选用复制算法
- 老年代:对象存活率高,反复复制开销大,采用标记整理或者标记清除算法。
引用计数
-
每个对象都有一个与之关联的引用数目
-
对象存活的条件:当且仅当引用数大于0
-
优点
- 内存管理的操作被平摊到程序执行过程中
- 内存管理不需要了解runtime的实现细节
-
缺点
- 维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性。
- 无法回收环形数据结构,即对象之间循环引用的问题。例如A对象引用了B对象,B对象也引用了A对象,两个对象都没有被使用到,应该要被回收,但是由于引用计数不为0无法被回收。
- 内存开销:每个对象都引入的额外空间存储引用数目。
- 回收内存时依然可能引发暂停。
GC算法评价标准
- 安全性(safety):不能回收存活的对象,是GC算法的基本要求
- 吞吐率(Throughout): 1-
- 暂停时间(Pause time):业务是否感知
- 内存开销(Space overhead):GC元数据开销
针对不同的业务,我们关注的指标也不同,比如对响应速度要求高的,面向用户的应用,需要关注暂停时间。而对一些响应要求不高的程序,可以关注提高吞吐率来尽可能提高cpu利用率。