在学习垃圾回收之前,我们需要明白下面这个知识点。
在Java虚拟机中,它会将它所管理的内存区域进行划分,也就是运行时所管理的内存区域,大致划分为以下几个区域:程序计数器、本地方法栈、虚拟机栈、方法区、堆、运行时常量池(实际它是在方法区中的)等。我们知道,前三个是线程私有的,随线程而生,随线程而死,后边的是属于线程共享的。由于前三个的特殊性,在回收时具有确定性,因此我们完全没有必要对这三个区域进行回收的设计,因为在线程消亡时就跟着回收。所以我们接下来的内容则是对方法区和堆进行垃圾回收的学习。

1 如何判断对象存活和不可再用

这里我们学习两个算法,引用计数算法和可达性分析算法

1.1 引用计数算法

引用计数算法则是每个对象在创建的时候,都会添加一个引用计数器,每当有个地方引用的时候,计数器加一,当引用失效时,计数器减一,当计数器为0时,代表着当前对象不可用,在GC时就可以进行回收。但是,该算法看起来方便快捷,但是存在着不少的问题,最显著的就是当两个对象互相引用的时候,标志就不可能为0,因此也不可能进行回收,并且在设置计数器的时候还占用一定的空间,因此目前的主流虚拟机是不使用这个算法的,所以,这个算法可以说是废弃了。

// 当存在下面这个情况的时候,问题就出现了,无法进行回收
class RCGC{
	private Object instance = null;

	@Test
	public void test(){
		RCGC r1 = new RCGC();
		RCGC r2 = new RCGC();
		r1.instance = r2;
		r2.instance = r1;
	}
}

1.2 可达性分析算法

在目前的主流虚拟机中,几乎都是在使用的可达性分析算法。我们先看下面这个图,这个算法的基本思想就是从被称为一系列的GC-Roots的对象作为起始节点向下进行搜索,走过的路径成为引用链,这时,有的对象到GC-Roots存在引用链,而有的对象没有,所以,没引用链的对象则是不可用的,面临着回收。

1.2.1 什么对象可以作为GC-Roots?
  1. 虚拟机栈(栈帧中的本地变量表)所引用的对象
  2. 本地方法中JNI(本地接口)所引用的对象
  3. 方法区中类静态属性所引用的对象
  4. 方法区中常量所引用的对象

1.3 4种引用类型

  1. 强引用
    在代码中普遍存在,如Object obj = new Object();,只要强引用存在,那么对象绝不会被GC。
  2. 软引用
    软引用是用来描述那些还有用但是非必需的对象,当内存即将发生溢出时,将会把它列入回收范围,在下次GC的时候,则进行回收。若内存足够,那它就不会被回收。若GC后内存依然不够,则会抛出内存溢出异常。在JDK1.2之后,提供SoftReference类实现软引用。
  3. 弱引用
    弱引用也是用来描述非必需对象的,只不过强度比软引用更弱一些,被软引用标识的对象,无论什么情况,在下次GC的时候,必定要进行回收,和内存足不足够无关!在JDK1.2之后,提供WeakReference类实现弱引用。
  4. 虚引用
    虚引用和垃圾回收的具体时间没有关系,也无法通过虚引用获得对象,被虚引用标记的对象,目的只是在垃圾回收的时候进行系统通知。在JDK1.2之后,提供PhantomReference类实现虚引用。

1.4 生存与死亡之间如何选择?

在进行可达性分析后,并不会立即对没有引用链的对象进行回收,回收中起码进行两次标记的过程。在判断是否与GC-Roots存在引用链后,对于存在引用链的则不回收,对于不存在引用链的对象进行第一次标记并且进行筛选,筛选的标准为是否有必要执行finalize()方法,,当对象没有覆盖finalize方法,或者虚拟机已经调用一次finalize方法,虚拟机认为这两种情况都是“没有必要执行”。
如果判断有必要执行,继续向下执行,将对象放置到F-queue队列中,并且由虚拟机创建的一个低优先级的Finalize线程执行方法,在这个过程中,Finalize线程是不等待方法执行完毕的,因为如果finalze方法执行缓慢,或者发生了更加极端的情况,那么就导致F-Queue中的其它对象都处于等待状态,从而导致GC系统失败。在finalize方法中,是对象逃离GC的最后一次机会,只要和拥有引用链的对象重新建立引用关系,就可以逃离GC。这时,则进行第二次标记,成功逃离的GC都被移出了“即将回收”的集合,没有逃离成功的就面临着GC。从下面的流程图和代码我们可以更加细致的明白这个过程。

运行结果: 这里需要大家明白一个道理,finalize方法只执行一次,所以第二次自救失败!
finalize method executed!
yes, i am still alive :)
no, i am dead :(

1.5 回收方法区

在Java虚拟机规范中,没有具体要求说明要对方法区进行回收,在堆中,回收新生代的性价比是非常高的,反而回收永久代的效率很低。永久代的垃圾回收主要有两个方面:常量池中的废弃常量和无用的类。废弃常量就是在常量池中的常量已经没有任何引用指向的一部分常量。而判断无用的类相比来说麻烦了许多,主要有下面三个条件:

  1. 该类的所有实例被回收
  2. 加载该类的ClassLoader被回收
  3. 该类的java.lang.Class对象没有在任何地方被引用,也就是没有在任何地方通过反射访问该类内容

满足这三个条件时,才可以进行回收,这里的可以回收还不是一定可以回收。是否对类进行回收,虚拟机还提供了很多参数进行设置,这里不进行列举,感兴趣的可以上网了解,反正就是判断无用的类较麻烦,这里一言两语还说不清楚!!!

2 垃圾回收算法

前面我们了解了如何判断对象的有用和无用,这里我们就学习如何去进行垃圾回收,也就是判断之后的具体解决办法。垃圾回收算法主要有四种:标记-清除算法、复制算法、标记-整理算法、分代收集算法。

2.1 标记-清除算法

标记-清除算法的思路为首先标记出无用的对象,即要回收的对象,在标记完成之后统一回收所有被标记的对象。并且它是一个举出算法,因为后边的算法都是以此为基础进行改进的。它主要有两个缺点:

  1. 效率问题:标记-清除两个过程效率都比较低
  2. 空间问题:在清除的时候,产生了大量的内存碎片,当后边需要分配较大的对象时,无法找到足够连续的内存空间不得不进行另一次的GC。

具体过程如图:

2.2 复制算法

复制算法的思想就是将内存按容量分成大小相等的两块,每次只使用其中的一块,当使用的这块内存容量使用完时,将可用对象存至另外一块内存中,使得它们之间是连续的,这样就不会出现内存碎片,而原来的内存块则进行清除。这种方法使用方便,简单高效,但是这种算法也有不合理之处,因为在实际的环境中,新生代中的对象有98%是“朝生夕死”的。没必要将内存划分成1:1的形式。所以在实际的虚拟机中,是将内存分为较大的Eden和两块较小的Survivor空间,这样将存活的对象移动到Survivor使得空间利用上更加合理高效,原来使用的Eden和Survivor直接清理。在HotSpot中,Eden:Survivor = 8:1,即新生代中的可用空间占全部新生代空间的90%,只有10%的空间浪费。但是,难免有一些意外情况,当存活的对象过多时,Survivor不足以存放所有的存活对象,这时候就需要“分配担保”机制来保证!就是利用其它内存进行存储过多的存活对象,这里的其它内存一般指的是老年代。

2.3 标记-整理算法

标记-整理算法和标记-清除算法类似,标记过程相同,只不过在整理过程中,它不像标记-清除算法那样直接进行清除,留下大量的内存碎片。它的思想是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。

2.4 分代收集算法

这个算法应该是现在主流虚拟机多采用的算法,由于新生代和老年代对象的特殊性,因此在不同代中采用不同的算法。在新生代中,由于98%(这里的数据不是肯定的)的对象“朝生夕死”,所以采用复制算法更加的合理。而在老年代中,对象的存活率非常高,采用复制算法效率很低,因此采用的是标记-清除算法和标记-整理算法。

3 HotSpot的算法实现

3.1 枚举根节点

前面我们学习了如何判断对象存活还是死亡。但是,在可达性分析算法中,如何去遍历GC-Roots呢?其实在虚拟机中,并非一一进行遍历,否则那样会浪费大量的时间。在GC的过程中,倘若在这个过程中一部分对象的引用又发生了变化怎么办呢?
这时,我们要保证对象不能在这个时间段发生引用上的变化,所以就要停下所有的Java执行线程,Java中称这个过程为Stop The World,非常的形象。当线程停下之后,就要检查Gc-Roots了。在HotSpot中,采用了OopMap的数据结构来实现的,在类加载完成时候,虚拟机就将对象内什么偏移量上是什么类型的数据进行计算并保存,这样,在GC的时候只需要查看OopMap就可以知道在那里寻找了。

3.2 安全点

在OopMap的协助下,也存在着不完美,如果为每个指令都生成OopMap,那么需要大量的空间,其实在HotSpot中,只是在特定的位置进行记录,这些位置称为安全点,即程序执行时并不能随时都可以进行gc,必须在特定的时候进行gc,这些特定的时候就是安全点。

3.3 安全区域

在实际情况中,可能还会遇到一部分线程处于sleep或者blocked状态等,这时,线程无法响应jvm的中断请求,这时,安全区域就出现了。安全区域指的是,在一段代码片段中,引用关系不会发生变化,在这个区域内,可以随时进行gc,即gc都是安全的,可以把安全区域认为是连续的安全点。当线程执行到安全区域的时候,进行标识,当jvm进行gc的时候,就可以直接进行了,当线程离开安全区域的时候,要检查jvm是否完成了根节点的枚举,没有的话,它必须要等待枚举完成即收到安全离开安全区域的信号为止。

4 垃圾回收器

目前所有的商用虚拟机都在使用的垃圾回收器,如下图

4.1 Serial收集器

  1. 单线程的收集器
  2. 进行垃圾回收时,必须暂停所有工作线程
  3. 运行在Client模式下
  4. 简单高效,由于没有线程交互的开销,收集的效率很高
  5. 可以和CMS和Serial Old进行搭配使用
  6. 新生代采用复制算法

4.2 ParNew收集器

  1. Serial的多线程版本
  2. 包括Serial的所有控制参数,收集算法,规则等
  3. 采用复制算法
  4. 运行在Server模式
  5. 除Serial外,只有它可以和CMS搭配使用

4.3 Parallel Scavenge收集器

  1. 并行的多线程收集器
  2. 达到可控制的吞吐量(运行客户代码时间/(运行客户代码时间+垃圾回收时间))
  3. 提供两个重要参数
    1. -XX:MaxGCPauseMills:最大垃圾停顿时间
    2. -XX:GCTimeRatio:吞吐量大小
  4. -XX:UserAdaptiveSizePolicy参数:根据运行情况对Eden、Survivor比例等进行动态调整。这种调节方式称为GC的自适应调节策略
  5. 自适应调节策略是和ParNew的重要区别

4.4 Serial Old收集器

  1. 单线程收集器
  2. 标记-整理算法
  3. Client模式
  4. 老年代
  5. CMS的后备预案

4.5 Parallel Old收集器

  1. 多线程收集器
  2. 标记-整理算法
  3. 和Parallel Scavenge搭配效率更佳

4.6 CMS收集器

  1. 获取最短回收停顿时间为目标的收集器
  2. Server模式
  3. 系统停顿时间短
  4. 标记清除算法
  5. 回收过程
    1. 初始标记(Stop The World):标记gc-roots能关联的对象
    2. 并发标记:gc-roots的tracing过程,可以和用户线程一起工作
    3. 重新标记(Stop The World):并发期间引用发生变化的对象
    4. 并发清除:可以和用户线程一起工作
  6. 明显缺点
    1. 对CPU资源非常敏感
    2. 无法处理浮动垃圾:并发清除阶段产生新的垃圾
    3. 产生内存碎片

4.7 G1收集器

  1. 收集器技术最前沿成果之一
  2. 面向服务端应用的垃圾收集器
  3. 特点:
    1. 并行与并发:充分利用多CPU,多核环境来缩短Stop The World时间,并且可以使用并发的方式让Java程序继续运行
    2. 分代收集:分代观念在G1中还是存在的,只不过不需要借助其它收集器就可处理新生代和老年代
    3. 空间整合:采用复制算法或标记整理算法,不会产生内存碎片
    4. 可预测的停顿:G1除了减小GC停顿时间外,还建立了可预测的时间模型,能让使用者明确指定一个毫秒时间段内,使得Gc的时间不会超过该时间
  4. 在G1中,将Java堆划分为多个大小相等的独立区域(Region),G1会跟踪每个Region,计算垃圾堆积的价值(回收垃圾所带来的空间大小以及所用的时间的经验值),并且在后台维护一个优先列表,在GC的时候回收优先级高的Region,这种策略,使得G1的回收效率非常的高。
  5. 回收步骤:
    1. 初始标记:STW,标记与gc-roots直接关联的对象
    2. 并发标记:可达性分析
    3. 最终标记:修改在并发标记中发生变化的对象,STW
    4. 筛选回收:回收价值分析

如有错误,欢迎指出