垃圾收集算法
写在前面
本文主要介绍以下几个内容:
- 怎么确定那些内存是垃圾: 引用计数法、可达性分析
- 什么时候回收垃圾:
- 怎么回收垃圾: 复制算法、标记-标记清除算法、标记整理算法、分代收集算法、分区收集算法、增量收集算法
1.怎么确定垃圾
1.1 垃圾标记的两种算法
算法 | 定义 | 优点 | 缺点 |
---|---|---|---|
引用计数算法 | 给对象添加一个计数器,每当有地方引用这个对象,计数器加1;引用失效时,计数器减1;计数器为0的对象就是不可用的。 | 判定简单,效率较高 | 无法解决对象之间的相互循环引用问题 |
可达性分析算法 | 通过一系列的“GC Roots” 的对象作为起始点,从这些节点向下搜索,搜索所走过的路径成为引用连(Reference Chain),当一个对象到GC Roots没有任何引用链,则证明这个对象是不可用的。 |
1.2 GC Roots的对象:
- 虚拟机栈(栈帧中局部变量表)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(也就是Native方法)引用的对象
1.3 生存还是死亡,finalize()方法
即使在可达性分析算法中不可达的对象,也并不是非死不可的,要宣告一个对死亡,至少经过两次标记过程,如下图。
2.怎么回收垃圾
2.1回收垃圾的几种算法
算法介绍
算法 | 定义 | 优点 | 缺点 | 备注 |
---|---|---|---|---|
标记-清除算法 (Mark-Sweep) | 分为标记(前文介绍的引用计数算法和可达性分析算法)和清除(收集器遍历堆内存的对象,如果对象的对象头没有标记为可达对象,则将其回收)两个阶段 | 简单,最基础的算法 | 1.效率不算高(标记需要递归根节点 ,清除需要遍历堆中的对象); 2.并且清除后会产生大量不连续的内存碎片,需要维护一个空闲列表 | 为什么不在标记阶段直接进行清除:因为标记阶段标记的是存活对象,而清除的是未存活对象,在标记阶段无法知道。 |
复制算法(Copying) | 将内存按容量大小分为大小相等的两块,每次只使用其中一块。当这块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。再交换两块空间的角色。 | 1.没有标记和清除过程,实现简单,运行高效; 2.复制的对象可以保证空间连续,没有内存碎片化的问题 | 1.如果对象存活率高,效率将变低; 2.每次只能使用一半空间,空间利用率低 ; 3.对于G1这种拆分为多个Region的GC? | 为什么没有标记过程:因为在可达性分析阶段就能确定那个对象是存活对象 |
标记-压缩算法 (Mark-Compact) | 标记同上,整理:让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。 | 1.解决了复制算法的内存利用率问题; 2.解决了标记-清除算法的内存碎片化问题 | 效率最低 | |
分代收集算法 | 将Java堆分为新生代和老年代,根据每个区域的特点选择不同的收集算法。在新生代,90%的对象朝生夕死,只有少数对象存活,选用复制算法。老年代对象存活率高。没有额外的空间进行分配担保,采用标记-清除或者标记-压缩算法。 | |||
增量收集算法 | 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程值收集一小片区域的内存,接着切换到应用程序线程,依次反复,直到垃圾收集完成。 | 减少了单次STW的时间,提高用户体验 | 线程切换和上下文转换的消耗,会使得垃圾回收的成本上升,造成系统的吞吐量下降 | 将总的收集量一部分一部分的去执行 |
分区收集算法 | 将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收 | 可以控制每次GC的停顿时间 | 将总的内存空间分为小分区,一次可控的去收集多少个小区间。 |
算法对比:
算法/比较项 | 标记-清除算法 | 复制算法 | 标记整理算法 |
---|---|---|---|
速度 | 次之 | 最快 | 最慢 |
空间开销 | 小(内存碎片) | 大 | 小(无内存碎片) |
对象移动 | 无 | 有 | 有 |
- 标记-清除算法
注:清除并不是将对象的内存空间清空,只是将这个可回收对象的地址加入空闲列表。(类似于电脑的格式化:格式化后,数据还在,只是将内存地址加入空闲列表,这时候可以让别的数据写进来,如果没有数据来进行覆盖,是可以恢复之前的数据的)
- 复制算法
- 标记-整理算法
3.什么时候回收(以Hotspot为例)
-内存空间满了则回收(感觉这里漏了点什么,留待以后补充吧)
4.补充几个知识点
4.1 关于引用
Java1.2以前,引用的定义很传统:如果Reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。但这过于 狭隘,一个对象只有引用和没有引用两种状态。比如我想描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存不够,则可以抛弃这些对象。
-
强引用 Strong Reference
只要引用还在,永不回收 -
软引用 Soft Reference 内存不足就回收
内存溢出之前,进行回收。如果回收后还没有足够的内存,才会OOM。 -
弱引用 Weak Reference 发现即回收
只能生存到下一次垃圾收集之前。 -
虚引用 Phantom Reference
一个对象是否有虚引用,完全不会对其生命周期构成影响,也无法通过取得虚引用来取得一个对象的应用。
4.2 Stop The Word(STW)
定义
在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
为什么要STW
可达性分析要求根节点和对象的引用关系不能发生变化,这就要求分析工作必须在同一个快照进行。为了保证一致性,需要STW。(本人自我见解:按上文,那么STW只需要发生在可达性分析的标记阶段就行,那么清除/压缩阶段需要STW么,个人感觉也是需要的。因为清除/压缩阶段需要遍历堆中的对象,进行清除或者移动。这也要求堆的一致性)
4.3 安全点Safe Point 和安全区域Safe Region
准确式GC
当系统停顿下来,不需要一个不漏 的检查完所有执行上下文和全局的引用位置,虚拟机有办法(OopMap数据结构)直接得知那些地方存着对象引用。 --可以快速且准确的完成GC Roots枚举
思考
维护 OopMap是需要耗费内存和Cpu的,不可能为了每条指令都去维护OopMap。那么问题来了,在什么时候维护OopMap,并且这需要考虑什么因素?
安全点Safe Point
只有 在特定的位置(也就是上文说的维护OopMap的指令位置),才能准确的枚举根节点,这些位置称为安全点(Safe Point)。只有在这些位置程序才能暂停。安全点的数目太多会影响性能;太少会让让GC等待是时间太长。
怎么选取安全点
以"是否 具有能让程序长时间执行的特征"为标准进行选取,比如方法调用、循环跳转、异常跳转等。
安全点的停顿方式:
- 抢占式中断:发生GC时,所有线程中断。如果某个线程不在安全点,就让它跑到安全点。
- 主动式中断:发生GC时,设置一个标志。线程执行到安全点,回去轮询这个标志,为真时就自己中断挂起。
安全区域
指在一段代码片段里,引用关系不会发生变化。在这个区域中的任意位置进行GC都是安全的。(这是为了解决程序不执行时的GC问题,安全点无法满足要求,难道要等程序执行到安全点么,万一代码sleep(10000),难道要等10秒后再进行GC)。
安全区域的进入和退出
在线程执行到安全区域中的代码时,首先标识自己已经进入了安全区域。那样,当GC时,会忽略掉这部分进程,不用中断它。在线程要离开安全区域时,会检查枚举根节点或者整个GC是否已经完成。如果已完成,则继续执行;否者就暂停,知道收到可以离开的信号。
4.4 垃圾回收的并行Parallel与并发Concurrent
并行
多个垃圾收集线程并行工作,但此时用户线程处于等待状态
并发
用户线程和垃圾收集线程同时执行(但不一定是并行,也可能是交替执行),用户程序再继续运行,而垃圾收集程序运行于另一个CPU上。