在 Java 的堆和方法区中,我们只有在程序处于运行期间才能知道创建了那些对象,这部分内存的分配和回收都是动态的,垃圾回收期关注的就是这部分内存.

1.1 对象已死吗

在堆中存放 Java 中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是确定那些对象”存活”,那些对象”死去”(不再被任何对象使用)

1.1.1 引用计数

给对象中添加一个引用计数器,每当有一个地方引用时,计数器值加1,当引用失效时,计数值就减1.当计数器值为0的对象就是不在被使用的.
优点是使用简单,缺点是很难解决对象之间相互循环引用的问题.

1.1.2 可达性分析算法

通过一系列的成为” GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Root 没有任何引用链时,就证明此对象是不可用的.
在 Java 语言中,可作为GC Root 的对象包括下面几种:

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI 引用的对象

1.1.3 再谈引用

在 JDK 1.2 之后 Java 对引用的概念进行了补充,添加了 强,软,弱,虚四中引用类型

  • 强引用:在程序中使用 new 关键字的引用属于强引用
  • 软引用:用来描述一些有用但并非必须的对象.使用软引用关联的对象,在内存泄露时会进行二次回收,如果回收还没有足够的内存才会跑出内存溢出.
  • 弱引用:也是用来描述非必需对象,但是比软引用更弱一些,当垃圾收集器工作时,无论内存是否足够,都会回收只被弱引用关联的对象
  • 虚引用:最弱的一种引用关系.一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法取得对象实例.

1.1.4 生存还是死亡

即使在可达性分析算法中不可达的对象,要经历两次标记过程才能宣告一个对象死亡.
当一个对象没有与 GC Root 链接的引用链,将会第一次标记并且进行筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,当没有覆盖或者已经执行 finalize()方法,虚拟机将视为”没有必要执行”.
当需要执行 finalize()方法,在 finalize()方法中重新与引用链上的一个对象关联即可拯救自己.

1.1.5 回收方法区

方法区中主要回收两部分内容:废弃的常量和无用的类.
判断废弃的常量,只需要判断有没有其他地方引用这个字面量.没有就会被清理出常量池.
判断一个无用的类,需要同时满足下面的3个条件.

  • 在 Java 堆中不存在该类的任何实例
  • 加载该类的 ClassLoader 已经被回收
  • 该类对象的 Class 对象没有任何地方呗引用,无法通过反射访问该类.

1.2 垃圾收集算法

1.2.1 标记-清除算法

首先标记出需要回收的对象,在标记完成后统一回收被标记的对象.
缺点是:效率低,标记清除后会产生大量不连续的内存碎片.

1.2.2 复制算法

将可用内存划分为两块,每次只使用一块,当一块用完,就将存活的对象复制到另一块,将已使用完的内存一次清理掉.
实现简单,运行高效,缺点是将内存缩小一半,代价太高.
很多商业虚拟机采用这种收集算法回收新生代.

1.2.3 标记-整理算法

标记过程与”标记-清除”算法一样,后续步骤先让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存.

1.2.4 分代收集算法

根据对象存活周期不同将内存划分为几块.一般是将 java 堆分为新生代和老年代,根据各个年代的特点采用适合的收集算法.

1.3 HotSpot 的算法实现

商业虚拟机的垃圾收集器采用”分代收集”算法,根据对象的存活周期将内存分为新生代和老年代,根据每个年代的特点采用不同的收集算法.

1.3.1 枚举根节点

从可达性分析中找引用链是一个很耗时的操作,可达性分析对执行时间的是在查找 GC的停顿上,因此这项工作必须确保快照的一致性,这里的一致性表示在分析过程就像被冻结在某个时间点上,当类加载完成之后, HotSpot 就将对象内偏移地址记录在一组成为 OomMap 的数据结构上.这样 GC 就可以直接得知这些信息.

1.3.2 安全点

HotSpot 没有为每条指定生成 OomMap 信息,而是在”特定的位置”记录了这些信息,这些位置成为安全点.即程序只有到达安全点才暂停执行 GC.

1.3.3 安全区域

安全区域是指在一段代码中,引用关系不会发生变化,在这个区域的任何地方开始 GC 都是安全的.

1.4 垃圾收集器

垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现.

1.4.1 Serial 收集器

单线程的收集器,在进行垃圾收集时,必须暂停所有其他的工作线程,直到收集结束.

1.4.2 ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本.

1.4.3 Parallel Scavenge 收集器

是一个新生代的收集器,特点是可以达到一个可控制的吞吐量.吞吐量指 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值.

1.4.4 Serial Old 收集器

是Serial收集器的老年代版本,也是单线程收集器,使用标记-整理算法.

1.4.5 Parallel Old收集器

是 Parallel Scavenge 收集器的老年代版本,使用多线程”整理-标记”算法.

1.4.6 CMS 收集器

以获取最短回收停顿时间为目标的收集器.运算过程更为复杂,分为初始标记,并发标记,重新标记,并发标记,4个步骤.

1.4.7 G1收集器

当前收集器技术最前沿成果之一,面向服务端应用的垃圾收集器.特点是:并行与并发,分代收集,可预测的停顿.

1.5 内存分配与回收策略

内存分配往大方向讲,就是在堆上分配,对象主要分配在新生代的 Eden 区上,如果启动了本地线程缓冲,按线程优先在 TLAB 上分配.少数情况下可能直接分配到老年代.

1.5.1 对象优先在 Eden 分配

大多数情况下,对象在新生代的 Eden 区分配,当 Eden 区没有足够空间,虚拟机将发起一次 Minor GC.

1.5.2 大对象直接进入老年代

大对象是指需要大量连续内存空间的 Java 对象,典型的如很长的字符串和数组.

1.5.3 长期存活的对象将进入老年代

为了区分那些对象应该在新生代或老年代,虚拟机给每个对象定义了一个对象年龄,如果对象在 Eden 出生并经过一次 Minor GC 后仍然存活,就会将对象年龄设为1,当年龄增加到一定程度(默认15岁),就会被晋升到老年代中

1.5.4 动态年龄判断

为了适应不同程序内存,虚拟机不是永远要求对象到达一定年龄才能晋升老年代,如果在 Survivor 空间中相同年龄对象大小的总和大于 Survivor 空间的一半,年龄大于或者等于该年龄对象可直接进入老年代.

1.5.5 空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用空间是否大于新生代所有对象总空间,如果条件成立,Minor GC 可以确保是安全的.如果不成立,虚拟机会查看是否允许担保失败,如果允许,会继续检查老年代最大可用空间是否大于历代晋升到老年代对象的平均大小,如果大于,将尝试进行 Minor GC,尽管这次 GC 是有风险的.如果小于,或者没有担保失败,就会进行一次 Full GC(老年代 GC).