Java垃圾收集

一、垃圾收集的算法

Java的自动垃圾收集,需要判断对象是否可以回收,判断的算法,是通过“可达性分析”算法,即通过一系列被称为“GC Root”的对象作为起点,从这些节点开始向下搜索,所走过的路径叫做引用链,当一个对象到GC Root没有任何引用链相连的时,就证明此对象可以回收。GC Root的可包含以下几种:

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native方法引用的对象
可达性分析算法的为了保证准确性,会短暂地暂停所有线程,称之为“Stop-The-World”。

常见的垃圾收集算法有如下几种:

1.1、标记-清楚算法

标记清除算法包含标记、清除两个步骤。这是个最基础的垃圾收集算法,后续的算法都是在此基础上进行改进的。改算法的好处是简单,劣势在于一是效率低,二是清除过后可能清理出的是断断续续的空间,不利于大对象的内存分配。

1.2、复制算法

复制算法的思路是将内存分为两块,当一块用完时,将存活的对象赋值到另一块中,并将原先的那块清理掉。这种算法实际上是以空间换取了效率。目前HotSpot是默认分配了Eden:From Survivor:To Survivor为8:1:1,因为很多对象都是“朝生夕死”,并不需要很大的替换空间。每次使用,都可以使用90%的空间(Eden+其中一个Survivor),清理时使用另一块Survivor承载存活的对象。当承载的Survivor空间不足时,将会由其他内存(比如老年代)进行分配担保。

1.3、标记-整理算法

基于标记清理算法上的优化,将存活的对象都统一向一个方向挪动,然后直接清理边界以外的内存。

二、垃圾收集器

HotSpot虚拟机使用了分代收集的方式,对不同年龄带的内存采用不同的垃圾收集算法。

Java垃圾收集

注:橙色为新生代,蓝色为老年代。

2.1、Serial / Serial Old

Serial是个针对新生代的单线程的收集器,在收集垃圾时,只会使用一个线程进行垃圾收集,而且它必须暂停所有的工作线程,直到它收集结束。Serial使用了复制算法。

Serial Old跟Serial是使用同样的算法,只是Serial Old是针对老年代的。Serial Old使用了标记-整理算法。

2.2、ParNew 

ParNew就是Serial收集器的多线程版本,也使用了复制算法。

2.3、Parallel Scavenge/ Parallel Old

Paralle Scavenge使用了复制算法,也是多线程处理,与ParNew不同的是,Paralle Scanvage的目标是达到一个可控制的吞吐量。吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾收集时间)。垃圾收集停顿的时间(不是垃圾收集时间)是跟新生代的大小有直接关系的(复制内存小的空间总是比大的空间节约时间的)。但是过小的新生代空间,又会不停地导致垃圾收集的触发。虚拟机提供了UseAdativeSizePolicy开关,如果将开关打开,虚拟机会动态调节,以提供合适的停顿时间和最大的吞吐量。

Parallel Old是Parallel Old的老年代版本,使用了多线程和标记-整理算法。

2.4、CMS

CMS是作用于老年代的,以获取最短回收停顿时间为目标的收集器。CMS基于标记-清除算法,过程分为四个步骤:

  • 初识标记,会STW,标记GC Root能关联到的对象。
  • 并发标记,不会STW,将初识标记的节点向下链接。

  • 重新标记,会STW,将并发标记过程中的程序的改动进行修正。
  • 并发清除,不会STW,并发清除垃圾内存。

2.5、G1

G1收集器将整个内存堆划分为多个大小相等的区域,从整体上对各个区域采用了标记-整理的算法,从单个区域来看,对单个区域使用了复制算法。G1收集器跟踪每个区域(Region)的垃圾堆的价值大小,根据允许的垃圾收集时间,优先收集价值最大的Region。

三、内存分配与回收

对象的内存分配,主要分配在新生代的Eden上,如果启动了本地线程分配缓冲(TLAB),会按线程优先分布在TLAB上。对象分配内存遵从以下的规则:

3.1、对象优先分配在Eden上

3.2、大对象直接进入老年代。虚拟机提供了参数PretenureSize参数,用于指定大对象的边界值。

3.3、长期存活的对象将进入老年代。每个对象都有个年龄计数器,每次Minor GC都会使得年龄计数器加1。当达到一定的年龄(默认15),就会直接晋升到老年代中。


注:借鉴《深入理解JAVA虚拟机》