垃圾收集算法

众所周知,Java程序不用像C/C++程序在程序中需要开发人员自行处理内存的回收/释放。这是因为Java在JVM虚拟机上增加了垃圾回收(GC)机制,用以在合适的时间触发垃圾回收,将不需要的内存空间回收释放,避免无限制的内存增长导致的OOM。作为一个合格的Java程序员,有必要了解Java GC相关知识。掌握GC知识一方面可以帮助我们快速排查因JVM导致的线上问题,另一方面也可以帮助我们在Java应用发布之前合理地对JVM进行调优,提高应用的执行效率、可靠性和健壮性。

 

1. Java堆内存结构

为了后面章节知识点的理解,我们应该先对Java堆内存结构划分有一定的了解。Java将堆内存分为3大部分:新生代、老年代和永久代,其中新生代又进一步划分为Eden、S0、S1(Survivor)三个区。结构如下图所示:

我们在程序中new出来的对象一般情况下都会在新生代里的Eden区里面分配空间,如果存活时间足够长将会进入Survivor区,进而如果存活时间再长,还会被提升分配到老年代里面。持久代里面存放的是Class类元数据、方法描述等。

(1)S0和S1是两个大小相等的区域,分配内存空间只会在其中某一个进行,另外一个空间是用来辅助进行新生代进行垃圾回收的,因为新生代的垃圾回收策略基于复制算法,其思想是将Eden区及两个Survivor中的某个区(如S0区)里面需要存活的对象复制到另外一个空的Survivor区(如S1区),然后就可以回收Eden和S0区域里面的死亡对象。下一次回收就对调S0和S1两个区的角色,S1用来存放存活对象而S0用来辅助回收垃圾,如此循环利用。 

(2)其实永久代就是我们所说的方法区,而方法区经常被称为Non-Heap(非堆)。仅仅在HotSpot虚拟机的实现中才将GC分代收集扩展至方法区,或者说使用永久代来实现方法区,对于其他的虚拟机是不存在永久代这个概念的。 

(3)并非所有的对象创建都会在Eden区中分配内存空间。对于Serial和ParNew垃圾收集器,通过指定-XX:PretenureSizeThreshold={size}来设置超过这个阈值大小的对象直接进入老年代。

 

2.引用计数算法(Reference Counting)

介绍:给对象添加一个引用计数器,每当一个地方引用它时,数据器加1;当引用失效时,计数器减1;计数器为0的即可被回收。

优点:实现简单,判断效率高

缺点:很难解决对象之间的相互循环引用(objA.instance = objB; objB.instance = objA)的问题,所以Java语言并没有选用引用计数法管理内存。

 

3.分代回收算法

我们一般讨论的垃圾回收主要针对Java堆内存中的新生代和老年代,也正因为新生代和老年代结构上的不同,所以产生了分代回收算法,即新生代的垃圾回收和老年代的垃圾回收采用的是不同的回收算法。针对新生代,主要采用复制算法,而针对老年代,通常采用标记-清除算法或者标记-整理算法来进行回收。

 

3.1根搜索算法(GC Root Tracing)

跟踪收集器采用的为集中式的管理方式,全局记录对象之间的引用状态,执行时从一些列GC  Roots的对象做为起点,从这些节点向下开始进行搜索所有的引用链,当一个对象到GC  Roots 没有任何引用链时,则证明此对象是不可用的。

下图中,对象Object6、Object7、Object8虽然互相引用,但他们的GC Roots是不可到达的,所以它们将会被判定为是可回收的对象。

可作为GC Roots 的对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中的引用对象。

  • 方法区中的类静态属性引用的对象

  • 方法区中的常量引用的对象

  • 本地方法栈中JNI的引用对象。

 

3.2 复制算法

复制算法的思想是将内存分成大小相等的两块区域,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块区域上,然后对该块进行内存回收。示例图如下所示:

主要缺点:

内存缩小为原来的一半。

这个算法实现简单,并且也相对高效,但是代价就是需要将牺牲一半的内存空间用于进行复制。有研究表明,新生代中的对象98%存活期很短,所以并不需要以1:1的比例来划分整个新生代,通常的做法是将新生代内存空间划分成一块较大的Eden区和两块较小的Survivor区,两块Survivor区域的大小保持一致。每次使用Eden和其中一块Survivor区,当回收的时候,将还存活的对象复制到另外一块Survivor空间上,最后清除Eden区和一开始使用的Survivor区。假如用于复制的Survivor区放不下存活的对象,那么会将对象存到老年代。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1:1,也就是说新生代中牺牲掉10%的空间而不是一半的空间。

 

3.3 标记-清除算法

标记清除算法是最基础的收集算法,其他收集算法都是基于这种思想。标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。

它的主要缺点:

①.标记和清除过程效率不高

②.标记清除之后会产生大量不连续的内存碎片。

标记阶段标记出需要回收的对象,然后在清除阶段里面,将这些标记出来的对象空间回收掉。这种算法有两个主要问题:一个是标记和清除的效率不高,另一个问题是在清理之后会产生大量不连续的内存碎片,这样会导致在分配大对象时候无法找到足够的连续内存而触发另一次垃圾收集动作。标记-清除算法示例图如下所示:

 

3.4 标记-整理算法

标记-整理(Mark-Compact)算法有效预防了标记-清除算法中可能产生过多内存碎片的问题。在标记需要回收的对象以后,它会将所有存活的对象空间挪到一起,然后再执行清理。示例图如下所示:

主要缺点:

在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片

标记-整理通常会在标记-清除算法里面作为备选方案,为了防止标记-清除后产生大量内存碎片而无法为大对象分配足够内存的情况,如后面所讲的Serial Old收集器(基于标记-整理算法实现)将会作为CMS收集器(基于标记-清除算法实现)的备选方案。

垃圾收集算法_第1张图片

 

你可能感兴趣的:(JVM,垃圾收集算法,GC,算法,标记清理,JVM内存模型)