JVM垃圾回收算法分析与演示【纯理论】

继续接着上一次【https://www.cnblogs.com/webor2006/p/10729649.html】的来学习,上次在结尾处提到了JVM常见的GC算法,如下:

接下来则逐一的对其进行学习,不过还是纯理论,比较枯燥但是必须得过一遍。

标记-清除算法(Mark-Sweep):

  • 算法分为“标记”“清除”两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象。
  • 缺点:
    a、效率问题:标记和清理两个过程效率都不高。
    b、空间问题:标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作。

下面用图表说明一下整个算法的过程,首先初始内存为:

可以看到从栈中有两个引用指向了堆,而堆中的对象存有互相引用的情况,其中左侧的栈可以称为Root GC,它可达的对象为A和B,如下:

其中标绿的是不能被GC回收的,接着继续:

最终内存的可达情况为:

其中标红的则是不能被Root GC所能引用的,也就是应该是被回收掉的,所以被回收之后整个图就变成了:

所以从上图的整个过程也能看到:

  • 效率不高,需要扫描所有对象,堆越大,GC越慢。
  • 存在内存碎片问题。GC次数越多,碎片越严重。 

复制搜集算法(Copying):

  • 将可用内存分为两块,每次只使用其中的一块,当半区内存用完了,仅将不存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。
  • 这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价高昂
  • 现在的商业虚拟机中都是用了这一种收集算法来回收新生代【啥叫新生代呢?通常情况下刚new出来的对象都会位于新生代当中,当新生代经历了几轮垃圾回收之后,尚未被回收的对象,这时JVM就会认为这些对象的存活时间比较长,则会将它们晋升到老年代中】
  • 将内存分为一块较大的eden空间和2块较少的survivor【幸存者】空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次性拷贝到另外一块survivor空间上,然后清理掉eden和用过的survivor。
  • Oracle Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有1%的内存是“浪费”的。
  • 复制收集算法在对象存活率高的时候,效率有所下降
  • 如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

下面则来用图来理解一下它的流程:

其中From-Space则是真正对象存放的空间,而To-Space则是垃圾回收之后接收被复制对象的空间,如上图,由于Stack引用了A和C,则会将A和C拷贝到To-Space空间中,而由于C引用了H、K,所以H也会进到To-Space中,如下:

另外由于H又引用了L,所以L也会进来,如下:

而又引用了B、E、I,则都会进来:

其中D、G是没有被Stack所引用,照理只回收这俩就成了,但是!!此时则会将From-Space全部给清空,如下:

下面再对其进行一些特点描述:

  • 只需要扫描存活的对象,效率更高。
  • 不会产生碎片。
  • 需要浪费额外的内存作为复制区。
  • 复制算法非常适合生命周期比较短的对象,因为每次GC总能回收大部分的对象,复制的开销比较小。
  • 根据IBM的专门研究,98%的Java对象只会存活1个GC周期,对这些对象很适合用复制算法。而且不用1:1的划分工作区和复制区的空间。

标记-整理算法(Mark-Compact):

它的标记过程跟上面的复制搜索算法是一样的,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。
如图:

可以看到下面就是进行了回收整理的空间,它的特点是:

  • 没有内存碎片
  • 比Mark-Sweep(标记清除算法)耗费更多的时间进行compact(压缩整理)

分代收集(Generational Collecting)算法

  • 当前商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collecting)算法,根据对象不同的存活周期将内存划分为几块。
  • 一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量对象的复制成本就可以完成收集。
  • 综合前面几种GC算法的优缺点,针对不同生命周期的对象采用不同的GC算法,比如:

    New代表新生代,如之前所说它里面的垃圾回收算法可以采用复制算法,而对于Old老年代的内存则可以通过标记清除算法或者标记整理算法。

  • Hotspot JVM6中共划分为三个代:年轻代(Young Generation)、老年代(Old Generation)、和永久代(Permanent Generation,注意这个是对于JDK8之前而言的,在JDK8之后此代是没有了)。如下图:

    在最开始时,对象是处于年轻代中,如下:

    然后对象是存在其中的Eden Space和From Space中,这俩的比例是可以调整的,其整个默认比例是8:1:1,而当进行垃圾回收时,则会将Eden Space和From Space留下来的对象都转到To Space上去,此时Eden Space和To Space又可以搭配工作,此时图中的From Space就变为了To Space,而图中的To Space又变成了From Space。而经过了几次回收之后,年轻代的对象就会进入到老年代,如下:

    而最终还有一个永久代,它是针对JDK8之前而存在的,如下:

    下面再具体来看一下各代的概念:

  • 年轻代(Young Generation)
    1、新生成的对象都放在新生代。年轻代用复制算法进行GC(理论上,年轻代对象的生命周期非常短,所以适合复制算法)。
    2、年轻代分三个区。一个Eden区,两个Survivor区(可以通过参数设置Survivor个数)。对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到一个Survivor区,当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当第二个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代。2个Survivor是完全对称,轮流替换。
    3、Eden和2个Survivor的缺省比例是8:1:1,也就是10%的空间会被浪费。可以根据GC log的信息调整大小的比例。
  • 老年代(Old Generation)
    1、存放了经过一次或多次GC还存活的对象。
    2、一般采用Mark-Sweep或者Mark-Compact算法进行GC。
    3、有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个GC算法的具体实现。可以根据具体应用的需求选用合适的垃圾收集器(追求吞吐量?追求最短的响应时间?)。
  • 永久代【注意:从JDK8开始已经用元空间来替代它了】
    1、并不属于堆(Heap)。但是GC也会涉及到这个区域。
    2、存放了每个Class的结构信息,包括常量池、字段描述、方法描述。与垃圾收集要收集的Java对象关系不大。

posted on 2019-04-20 10:13  cexo  阅读(847)  评论(0编辑  收藏  举报

导航