如何判断对象可以回收
引用计数法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
1 | public class ReferenceCountingGC { |
因为循环引用的存在,所以 Java 虚拟机不适用引用计数算法。
可达性分析法
通过 GC Roots 作为起始点进行搜索,JVM 将能够到达到的对象视为存活,不可达的对象视为死亡。
可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象(Native 方法)
- 方法区中,类静态属性引用的对象
- 方法区中,常量引用的对象
引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否可被回收都与引用有关。
Java 具有四种强度不同的引用类型。
- 强引用
被强引用(Strong Reference)关联的对象不会被垃圾收集器回收。
强引用:使用new一个新对象的方式来创建强引用。
1 | Object obj = new Object(); |
- 软引用
被软引用(Soft Reference)关联的对象,只有在内存不够的情况下才会被回收。
软引用:使用SoftReference类来创建软引用。
1 | Object obj = new Object(); |
- 弱引用
被弱引用(Weak Reference)关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。
使用WeakReference类来实现弱引用。
1 | Object obj = new Object(); |
- 虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
使用PhantomReference来实现虚引用。
1 | Object obj = new Object(); |
finalize()
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用 finalize()。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。
垃圾收集算法
垃圾收集性能
垃圾收集器的性能指标主要有两点:
- 停顿时间 - 停顿时间是因为 GC 而导致程序不能工作的时间长度。
- 吞吐量 - 应用程序运行时间占总运行时间的比例,
吞吐量 = 应用程序运行时间 / (应用程序运行时间 + GC时间)
标记 - 清除(Mark-Sweep)
将需要回收的对象进行标记,然后清理掉被标记的对象,速度比较快。
不足:
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
标记 - 整理(Mark-Compact)
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销,速度较慢。
复制(Copying)
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
不会存在内存碎片,主要不足是会使用双倍内存。
现在的商业虚拟机都采用这种收集算法来回收年轻代
分代垃圾回收
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将 Java 堆分为年轻代和老年代。
- 年轻代使用:复制 算法
- 老年代使用:标记 - 清理 或者 标记 - 整理 算法
新生代
新生代是大部分对象创建和销毁的区域,在通常的 Java 应用中,绝大部分对象生命周期都是很短暂的。其内部又分为 Eden(伊甸园) 区域,作为对象初始分配的区域,当Eden区满时,会触发一次Minor GC(年轻代垃圾回收);两个 Survivor,有时候也叫 from、to 区域,被用来放置从 Minor GC(次要垃圾回收) 中保留下来的对象。
- 在 Minor GC 中,
Eden区中仍然存活的对象会被移动到其中一个Survivor区(比如from区); - 同时,如果
from区中已经有对象,那么from区中存活的对象也会被移动到to区; - 然后,清空
Eden区和from区,此时from区变成空的,而to区存放了从Eden和from区中存活下来的对象。 - 接下来,
from和to的角色会交换。也就是说,下一次 Minor GC 时,当前的to区将作为from区(即存活对象被移出的区),当前的from区将作为to区(即存活对象被移入的区)。
Survivor 区域是JVM随意选择的,然后会在 GC 过程中进行区域间拷贝,也就是将 Eden 中存活下来的对象和 from 区域的对象,拷贝到这个to区域。这种设计主要是为了防止内存的碎片化,并进一步清理无用对象。
Java 虚拟机会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代。另外,如果单个 Survivor 区已经被占用了 50%(对应虚拟机参数 -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。
老年代
放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。当然,也有特殊情况,如果对象较大,JVM 会试图直接分配在 Eden 其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代。
永久代
这部分就是早期 Hotspot JVM 的方法区实现方式了,储存 Java 类元数据、常量池、Intern 字符串缓存。在 JDK 8 之后就不存在永久代这块儿了。
JVM 参数
这里顺便提一下,JVM 允许对堆空间大小、各代空间大小进行设置,以调整 JVM GC。
| 配置 | 描述 |
|---|---|
-Xss |
虚拟机栈大小。 |
-Xms |
堆空间初始值。 |
-Xmx |
堆空间最大值。 |
-Xmn |
新生代空间大小。 |
-XX:NewSize |
新生代空间初始值。 |
-XX:MaxNewSize |
新生代空间最大值。 |
-XX:NewRatio |
新生代与年老代的比例。默认为 2,意味着老年代是新生代的 2 倍。 |
-XX:SurvivorRatio |
新生代中调整 eden 区与 survivor 区的比例,默认为 8。即 eden 区为 80% 的大小,两个 survivor 分别为 10% 的大小。 |
-XX:PermSize |
永久代空间的初始值。 |
-XX:MaxPermSize |
永久代空间的最大值。 |
垃圾回收器
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
注:G1 垃圾收集器既可以回收年轻代内存,也可以回收老年代内存。而其他垃圾收集器只能针对特定代的内存进行回收。
串行收集器
串行收集器(Serial)是最基本、发展历史最悠久的收集器。
串行收集器是 client 模式下的默认收集器配置。因为在客户端模式下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的年轻代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
串行收集器采用单线程 stop-the-world 的方式进行收集。当内存不足时,串行 GC 设置停顿标识,待所有线程都进入安全点(Safepoint)时,应用线程暂停,串行 GC 开始工作,采用单线程方式回收空间并整理内存。
Serial / Serial Old 收集器运行示意图
单线程意味着复杂度更低、占用内存更少,垃圾回收效率高;但同时也意味着不能有效利用多核优势。事实上,串行收集器特别适合堆内存不高、单核甚至双核 CPU 的场合。
Serial 收集器
开启选项:
-XX:+UseSerialGC打开此开关后,使用 Serial + Serial Old 收集器组合来进行内存回收。
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
🔔其他收集器以后再说
并发标记清除收集器
开启选项:
-XX:+UseConcMarkSweepGC打开此开关后,使用 CMS + ParNew + Serial Old 收集器组合来进行内存回收。
并发标记清除收集器是以获取最短停顿时间为目标。
开启后,年轻代使用 ParNew 收集器;老年代使用 CMS 收集器,如果 CMS 产生的碎片过多,导致无法存放浮动垃圾,JVM 会出现 Concurrent Mode Failure ,此时使用 Serial Old 收集器来替代 CMS 收集器清理碎片。
CMS 收集器
CMS 收集器是一种以获取最短停顿时间为目标的收集器。
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
CMS 回收机制
CMS 收集器运行步骤如下:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:回收在标记阶段被鉴定为不可达的对象。不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
CMS 收集器运行示意图
CMS 回收年轻代详细步骤
(1)堆空间被分割为三块空间

年轻代分割成一个 Eden 区和两个 Survivor 区。年老代一个连续的空间。就地完成对象收集。除非有 FullGC 否则不会压缩。
(2)CMS 年轻代垃圾收集如何工作
年轻代被标为浅绿色,年老代被标记为蓝色。如果你的应用已经运行了一段时间,CMS 的堆看起来应该是这个样子。对象分散在年老代区域里。

使用 CMS,年老代对象就地释放。它们不会被来回移动。这个空间不会被压缩除非发生 FullGC。
(3)年轻代收集
从 Eden 和 Survivor 区复制活跃对象到另一个 Survivor 区。所有达到他们的年龄阈值的对象会晋升到年老代。

(4)年轻代回收之后
一次年轻代垃圾收集之后,Eden 区和其中一个 Survivor 区被清空。

最近晋升的对象以深蓝色显示在上图中,绿色的对象是年轻代幸免的还没有晋升到老年代对象。
CMS 回收年老代详细步骤
(1)CMS 的年老代收集
发生两次 stop the world 事件:初始标记和重新标记。当年老代达到特定的占用比例时,CMS 开始执行。

- 初始标记是一个短暂暂停的、可达对象被标记的阶段。
- 并发标记寻找活跃对象在应用连续执行时。
- 最后,在重新标记阶段,寻找在之前并发标记阶段中丢失的对象。
(2)年老代收集-并发清除
在之前阶段没有被标记的对象会被就地释放。不进行压缩操作。

**注意:**未被标记的对象等于死亡对象
(3)年老代收集-清除之后
清除阶段之后,你可以看到大量内存被释放。你还可以注意到没有进行压缩操作。

最后,CMS 收集器会再次进入重新设置阶段,等待下一次垃圾收集时机的到来。
CMS 特点
CMS 收集器具有以下缺点:
- 并发收集 - 并发指的是用户线程和 GC 线程同时运行。
- 吞吐量低 - 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾 - 可能出现
Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。- 可以使用
-XX:CMSInitiatingOccupancyFraction来改变触发 CMS 收集器工作的内存占用百分,如果这个值设置的太大,导致预留的内存不够存放浮动垃圾,就会出现Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 收集器来替代 CMS 收集器。
- 可以使用
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
- 可以使用
-XX:+UseCMSCompactAtFullCollection,用于在 CMS 收集器要进行 Full GC 时开启内存碎片的合并整理,内存整理的过程是无法并发的,空间碎片问题没有了,但是停顿时间不得不变长了。 - 可以使用
-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的 Full GC 后,来一次带压缩的(默认为 0,表示每次进入 Full GC 时都要进行碎片整理)。
- 可以使用
ParNew 收集器
开启选项:
-XX:+UseParNewGC
ParNew 收集器其实是 Serial 收集器的多线程版本。
ParNew 收集器运行示意图
是 Server 模式下的虚拟机首选年轻代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
ParNew 收集器也是使用 -XX:+UseConcMarkSweepGC 后的默认年轻代收集器。
ParNew 收集器默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
G1 收集器
开启选项:
-XX:+UseG1GC
前面提到的垃圾收集器一般策略是关注吞吐量或停顿时间。而 G1 是一种兼顾吞吐量和停顿时间的 GC 收集器。G1 是 Oracle JDK9 以后的默认 GC 收集器。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。
G1 最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至 CMS 的众多缺陷。
分代和分区
旧的垃圾收集器一般采取分代收集,Java 堆被分为年轻代、老年代和永久代。收集的范围都是整个年轻代或者整个老年代。
G1 取消了永久代,并把年轻代和老年代划分成多个大小相等的独立区域(Region),年轻代和老年代不再物理隔离。G1 可以直接对年轻代和老年代一起回收。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
G1 回收机制
G1 收集器运行示意图
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记 - 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收 - 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
G1 回收年轻代详细步骤
(1)G1 初始堆空间
堆空间是一个被分成许多固定大小区域的内存块。

Java 虚拟机启动时选定区域大小。Java 虚拟机通常会指定 2000 个左右的大小相等、每个大小范围在 1 到 32M 的区域。
(2)G1 堆空间分配
实际上,这些区域被映射成 Eden、Survivor、年老代空间的逻辑表述形式。

图片中的颜色表明了哪个区域被关联上什么角色。活跃对象从一个区域疏散(复制、移动)到另一个区域。区域被设计为并行的方式收集,可以暂停或者不暂停所有的其它用户线程。
明显的区域可以被分配成 Eden、Survivor、Old 区域。另外,有第四种类型的区域叫做极大区域(Humongous regions)。这些区域被设计成保持标准区域大小的 50%或者更大的对象。它们被保存在一个连续的区域集合里。最后,最后一个类型的区域就是堆空间里没有使用的区域。
**注意:**写作此文章时,收集极大对象时还没有被优化。因此,你应该避免创建这个大小的对象。
(3)G1 的年轻代
堆空间被分割成大约 2000 个区域。最小 1M,最大 32M,蓝色区域保持年老代对象,绿色区域保持年轻代对象。

**注意:**区域没有必要像旧的收集器一样是保持连续的。
(4)G1 的年轻代收集
活跃对象会被疏散(复制、移动)到一个或多个 survivor 区域。如果达到晋升总阈值,对象会晋升到年老代区域。

这是一个 stop the world 暂停。为下一次年轻代垃圾回收计算 Eden 和 Survivor 的大小。保留审计信息有助于计算大小。类似目标暂停时间的事情会被考虑在内。
这个方法使重调区域大小变得很容易,按需把它们调大或调小。
(5)G1 年轻代回收的尾声
活跃对象被疏散到 Survivor 或者年老代区域。

最近晋升的对象显示为深蓝色。Survivor 区域显示为绿色。
关于 G1 的年轻代回收做以下总结:
- 堆空间是一块单独的内存空间被分割成多个区域。
- 年轻代内存是由一组非连续的区域组成。这使得需要重调大小变得容易。
- 年轻代垃圾回收是 stop the world 事件,所有应用线程都会因此操作暂停。
- 年轻代垃圾收集使用多线程并行回收。
- 活跃对象被复制到新的 Survivor 区或者年老代区域。
G1 回收年老代详细步骤
(1)初始标记阶段
年轻代垃圾收集肩负着活跃对象初始标记的任务。在日志文件中被标为GC pause (young)(inital-mark)

(2)并发标记阶段
如果发现空区域(“X”标示的),在重新标记阶段它们会被马上清除掉。当然,决定活性的审计信息也在此时被计算。

(3)重新标记阶段
空的区域被清除和回收掉。所有区域的活性在此时计算。

(4)复制/清理阶段
G1 选择活性最低的区域,这些区域能够以最快的速度回收。然后这些区域会在年轻代垃圾回收过程中被回收。在日志中被指示为*[GC pause (mixed)]*。所以年轻代和年老代在同一时间被回收。

(5)复制/清理阶段之后
被选择的区域已经被回收和压缩到图中显示的深蓝色区和深绿色区中。

总结
| 收集器 | 串行/并行/并发 | 年轻代/老年代 | 收集算法 | 目标 | 适用场景 |
|---|---|---|---|---|---|
| Serial | 串行 | 年轻代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
| Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
| ParNew | 串行 + 并行 | 年轻代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
| Parallel Scavenge | 串行 + 并行 | 年轻代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| Parallel Old | 串行 + 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| CMS | 并行 + 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
| G1 | 并行 + 并发 | 年轻代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |







