JVM内存回收-垃圾收集算法
分代收集理论
当前主流虚拟机的垃圾收集器,大多数都遵循了”分代收集”的理论进行设计, 分代收集名为理论,实质上是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说上:
- 弱分代假说: 绝大多数对象都是朝生夕灭的
- 强分代假说: 熬过越多次垃圾收集过程的对象就越难以消亡
这两个假说共同奠定了常用垃圾收集器的设计原则: 收集器应该将Java堆划分出不同的区域,然后根据回收对象的年龄(熬过收集过程的次数),分配到不同的区域之中.
在Java堆划分出不同的区域后,垃圾收集器就可以每次只回收某一个类型的区域,因而才有了Minor GC
,Major GC
,Full GC
这样的回收类型划分.
但实际上,分代收集并非只是简单的划分了下区域那么简单,它至少存在一个困难: 对象并非孤立存在,可能会有跨代引用
假如现在要对新生代区域进行一次垃圾收集,新生代的对象是完全有可能被老年代所引用的,为了找到这类对象,就不得不将整个老年代也作为GC Root
.
这样虽然可以确保这些对象不会被回收,但是也带来了极大的性能负担. 因此在这基础上,对分代收集理论添加了第三条经验法则:
- 跨引用假说: 跨代引用相对于同代引用是极少数的
很显然这条推论是基于前面两条假说得到的隐藏推论,也可以换一种方式理解: 存在互相引用关系的两个对象应该是倾向于同时生存或同时消亡的.
根据这条推论,我们就不需要为了少量的跨代引用去扫描整个老年代,只需在新生代上建立一个全局的数据结构(Remembered Set,即记忆集),用于标识老年代的哪一块内存区域存在跨代引用.
此后发生Minor GC
时,会加入到GC Roots
中
GC的种类划分:
- 部分收集: 指目标不是整个Java堆的垃圾收集器
- 新生代收集(Minor GC/Young GC): 指目标是新生代的垃圾收集
- 老年代收集(Major GC/Old GC): 指目标是老年代的垃圾收集
- 混合收集(Mixed GC): 指目标是收集整个新生代已经部分老年代的垃圾收集
- 整堆收集(Full GC): 收集整个Java堆和方法区的垃圾收集
标记-清除算法(Mark-Sweep)
最早出现也是最基础的垃圾收集算法是”标记-清除”算法,过程分为”标记”和”清除”两个阶段:
- 标记阶段: 标记出所有不需要回收的对象
- 清除阶段: 统一回收所有未被标记的对象
它的主要缺点有两个:
- 执行效率不稳定, 标记和清除两个过程的执行效率都依赖于当前Java堆中的对象数量,数量越多,效率越低
- 其次就是容易造成大量的不连续的内存碎片,内存碎片太多可能会导致程序后续运行中分配较大对象时无法找到连续内存而不得不再次触发垃圾收集过程
标记-复制算法(Mark-Copy)
为了解决标记-清除算法面对大量回收对象时执行效率低下的问题, 1969年Fenichel提出了一种称为”半区复制”的垃圾收集算法.
它将内存按容量划分成为两种大小相等的两块区域,每次只使用其中一块,当某一块区域的内存用完了,就将存活的对象复制到另一块空闲区域上,然后原有区域的内存空间一次性释放. 如果内存中大多数对象都是存活的,这种算法将会在复制阶段产生大量开销,但是对于多数对象都是可回收的情况,这种做法的性能就很好. 另外的,在分配(复制)内存时,也不需要考虑空间碎片的问题,直接在区域上按顺序分配即可.
但这种方式的代价就是堆中有一半的内存可用,空间浪费程度较大.
IBM公司曾有一项专门研究对新生代对象”朝生夕灭”的特点做了更加量化的诠释: 新生代中的对象有98%熬不过第一轮收集. 因此不需要按照1:1的比例来划分新生代的空间
在1989年,Andrew Appel针对上面的结论,提出了一种更优化的半区复制分代策略:
- 将新生代分为一块较大的Eden区和两块较小的Survivor区
- 每次分配内存只使用Eden区和其中一块Survivor区
- 发生垃圾收集时,将Eden和已使用过的那块Survivor区,复制到另外一块Survivor区上,然后直接清理掉Eden区和原先使用的Survivor区
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1. 当然,98%的对象可回收仅是实验数据,任何人都无法保证每次回收都只有不多于10%的对象存活,因此还有一个额外的”逃生门”的设计:
- 当Survivor空间不足以容纳当前Minor GC存活的对象后,就需要依赖其他区域(实际上就是老年代)来进行分配
标记-整理算法(Mark-Compact)
标记复制算法在对象存活率较高时就需要进行比较多的复制操作,效率就会降低,同时还存在着一定的内存空间浪费. 同时还需要有额外空间进行担保,以应对内存中所有对象都100%存活的极端情况,所以在老年代一般不能选用这种算法
针对老年代对象的存活特征,1974年Edward Lueders提出了另外一种有针对性的”标记-整理”算法. 其中标记过程仍然与”标记-清除”算法保持一致,但后续阶段,不是直接对可回收对象进行回收后,会将剩余存活对象都向内存一端移动,然后直接清理掉边界以外的内存