简介
由于《分代收集理论》和不同垃圾收集算法,Java堆应该被划分为不同区域,一般至少会把Java堆划分为新生代(Young Generation)
和老年代(Old Generation)两个区域
。
垃圾收集器可以只回收其中某一个或者某些部分的区域对应不同的垃圾回收:
“Minor GC”:新生代的垃圾收集
“Major GC”:老年代垃圾收集
“Full GC”:整个Java堆和方法区的垃圾收集
默认的,新生代( Young ) 与老年代( Old ) 的比例的值为 1:2即:新生代( Young ) = 1/3 的堆空间大小。 老年代( Old ) = 2/3 的堆空间大小 ( 该值可以通过参数–XX:NewRatio 来指定)
新生代又划分为一块Eden和两块Survivor(占比新生代的80%、10%、10%)
标记-清除算法
算法分为“标记”和“清除”两个阶段:
“标记”
:标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象(标记过程就是对象是否属于垃圾的判定过程
)
“清除”
:回收标记对象
两大缺点
1、执行效率不稳定
如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。
2、内存空间的碎片化
标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-复制算法
标记-复制算法常被简称为复制算法,为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块
。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
存在问题
如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销。
但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。(实现简单、运行高效)
是将可用内存缩小为了原来的一半,会造成空间浪费。
实际上
新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor
。
发生垃圾回收时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。
新生代中垃圾回收时,如果有多于10%对象存活,此时一个Survivor空间不足以容纳一次Minor GC之后存活的对象,就需要依赖其他内存区域(实际上大多就是老年代)
。
标记-整理算法
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法
。
“标记-整理”(Mark-Compact)算法:
“标记”
:标记出所有需要回收的对象
“整理”
:让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式
的回收算法,而后者是移动式
的。
存在问题
移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方是一种极为负重的操作
,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿被最初的虚拟机设计者形象地描述为“Stop The World
”。
垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。
不移动对象会使得收集器的效率提升一些,但因内存分配和访问相比垃圾收集频率要高得多,这部分的耗时增加,总吞吐量仍然是下降的。
“和稀泥式”解决方案
让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间
。
CMS收集器面临空间碎片过多时采用的就是这种处理办法。