垃圾回收算法

主要有两种算法:引用计数式垃圾收集(Reference Counting GC)和追踪式垃圾收集(Tracing GC)。

分代假说:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

基于这两个假说,引出了垃圾收集器的设计原则:收集器应将Java堆划分出不同的区域,然后将对象根据其年龄(熬过垃圾收集的次数)分配到不同的区域中。如果一个区域中大多数对象都是朝生夕灭的话,那么每次回收只需要关注如何保留存活下来的对象;如果一个区域都是难以消亡的对象,那么把它们集中放在一起,虚拟机就能以较低的频率来回收这个区域。

设计者一般将Java堆划分为新生代和老年代两个区域。在新生代中,每次垃圾收集时都有大量对象死去,而每次回收后存货的少量对象,将会逐步晋升到老年代存放。

分代收集会带来一个明显的问题,对象之间存在跨代引用,如果老年代的对象引用了新生代中的对象,那么为了判断新生代中对象是否存活,还需要在固定的GC Roots之外额外遍历老年代中的所有对象。这样会带来很大的性能问题。为了解决这个问题,需要对分代收集理论添加第三条经验法则:

  1. 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

根据这条假说,我们就不应该为了少量的跨代引用去扫描整个老年代,也不需要专门记录每一个对象是否存在及存在哪些跨代引用,只需要在新生带上建立一个全聚德数据结构(记忆集,Remembered Set),这个结构把老年代划分为若干小块,标识出老年代的哪一块内存会存在跨代引用。当发生Minor GC(对新生代进行垃圾回收)时,只需要把存在跨代引用的老年代对象加入GC Roots中进行扫描。这种方法需要在对象改变引用关系的时候维护数据的正确性,但是相对扫描整个老年代来说仍然划算。

垃圾收集的名词

  • 部分收集:指目标不是整个Java堆
    • 新生代收集(Minor GC/Young GC):目标只是新生代
    • 老年代收集(Major GC/Old GC):目标只是老年代,目前只有CMS收集器会有单独收集老年代的行为
    • 混合收集(Mixed GC):目标是整个新生代以及部分老年代。目前只有G1收集器会有这种行为
  • 整堆收集:收集整个Java堆和方法区

标记-清除算法

最早出现也是最基础的垃圾收集算法。算法分为两个阶段

  • 标记:标记出所有需要回收的对象
  • 清除:统一回收掉所有被标记的对象

也可以反过来标记存活的对象,回收没有被标记的对象。

缺点:

垃圾回收时会进行大量标记和清除的动作,导致算法效率降低;标记清楚之后会产生大量不连续的内存碎片,导致在之后需要为较大的对象分配内存时出现无法找到足够大的连续内存,从而触发另一次垃圾回收。

标记-复制算法

也叫做半区复制算法。将内存划分为大小相等的两块,每次只使用其中一块,垃圾收集时将所有存活的对象复制到另一块上,然后把原来的内存空间全部清理掉。对于内存中只有少量对象存活的情况效率高,而且也没有内存碎片产生,不过缺点是可用内存缩小为了原来的一半。

IBM做过研究,新生代中的对象有98%熬不过第一轮收集,所以并不需要按照1 : 1的比例划分新生代的内存空间。

Appel式回收

是一种更优的半区复制分代策略。把新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存货的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已经使用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8 : 1。这样每次可以使用的新生代内存占整个新生代内存的90%。

但是无法保证每次存活的对象占用内存不多于10%,所以当Survivor空间不足以容纳一次Minor GC之后存货的对象是,就需要依赖其它内存区域(大多是老年代)进行分配担保。这些对象通过分配担保机制直接进入老年代。

标记-整理算法

标记-复制算法在对象存活率较高的时候需要进行较多的复制操作,效率会降低。而且需要额外的空间进行分配担保,所以在老年代一般不使用这种算法。

算法过程也是先标记,然后将所有存活的对象都向内存空间的一端移动,然后清理掉边界以外的内存。

标记-清除算法和标记-整理算法的对比

如果移动存活对象的时候,必须更新所有引用这些对象的地方,在老年代这是一种消耗极大的操作,而且这种操作需要暂停应用程序才能执行。

如果不移动存活对象的话,存活对象造成的空间碎片需要通过类似于”分区空闲分配链表“来解决内存分配的问题。但是用户对于内存的访问十分频繁,如果在这个环节上增加额外的负担,就会直接影响应用程序的吞吐量
吞吐量 = 运行用户代码时间 运行用户代码时间 + 运行垃圾回收时间 吞吐量=\frac{运行用户代码时间}{运行用户代码时间+运行垃圾回收时间} 吞吐量=运行用户代码时间+运行垃圾回收时间运行用户代码时间
因此还有另外一种解决方案

让虚拟机多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次。

你可能感兴趣的:(java,jvm,算法)