Java堆
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
下面我们来具体介绍Java堆中的分代。
年轻代
年轻代主要分为两个区,分别是Eden区和2个Survivor区。年轻代存在的意义是让GC尽可能快速地收集生命周期较短的对象。年轻代内存示意图如下:
Tip: Eden即伊甸园,传说此处是人类的起源之地,故对象刚被创建出来被放入Eden区。

从图中可以看出年轻代内存分为一块Eden区和两块较小的Survivor空间,分别是8:1:1的比例。当进行垃圾回收时,将Eden和Survivor中存活的对象一次性复制到另一块Survivor空间中,最后清理Eden和用掉的Survivor空间。当Survivor空间都不够用时,则需要老年代进行分配收集。
大部分情况下,对象都会先在Eden区进行分配(除非如Eden区已满),在一次年轻代垃圾回收后,如果对象还存活,则对象会从Eden区进入 s0 或者 s1区域中,并且对象的年龄加 1,当它的年龄增长到阈值时(默认为大于 15 岁),就会被晋升到老年代中,可以通过参数 -XX:MaxTenuringThreshold 来设置该阈值。
常用的调优参数:
- -XX:SurvivorRatio:Eden和Survivor的比值,默认8:1。
- -XX:NewRatio:老年代和年轻代内存大小的比例。
- -XX:MaxTenuringThreshold:对象从年轻代晋升到老年代经过GC次数的最大阈值。
老年代
老年代存放生命周期较长的对象。以下对象会进入到老年代中:
- 经历一定Minor次数依然存活的对象,即默认年龄大于15(Minor我们下文会进行介绍)
- Survivor区中存放不下的对象
- 新生成的大对象
判断一个对象是否可被回收的方法
引用计数法
- 通过判断对象的引用次数来决定对象是否可被回收。
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1。
- 任何引用计数为0的对象实例可以被当作垃圾收集。
优点:执行效率高,程序执行受影响较小。
缺点:无法检测出循环引用的情况,容易导致内存泄露。循环引用例子如下:
package com.yeliheng.gc;
public class ReferenceCountingGC {
Object ref = null;
public static void main(String[] args) {
ReferenceCountingGC o1 = new ReferenceCountingGC();
ReferenceCountingGC o2 = new ReferenceCountingGC();
o1.ref = o2;
o2.ref = o1;
}
}
o1和o2两个对象互相引用对方,除此之外没有其他任何引用。但由于彼此之间循环引用,导致它们的引用计数器永远不会为0,所以使用引用计数法无法让GC对其进行回收。
可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示,可达性分析算法以GC Root为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。

可作为GC Root的对象
- 虚拟机栈中引用的对象(栈帧中的本地变量表)。
- 方法区中的常量引用的对象。
- 方法区中的类静态属性引用的对象。
- 本地方法栈中JNI即Native方法的引用对象。
- 活跃线程的引用对象。
垃圾回收算法
标记-清除算法(Mark and Sweep)
- 标记:从根集合进行扫描,对存活的对象进行标记。
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存。
它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
缺点:会产生大量内存碎片,导致无法给大对象分配内存。
复制算法(Copying)
复制算法将内存分为对象面和空闲面,对象在对象面上创建,把存活的对象从对象面复制到空闲面,再将对象面上所有的内存清除。此算法解决了碎片化的问题,按顺序分配内存,简单高效,适用于对象存活率低的场景。

标记-整理算法(Compacting)
标记整理算法会标记出所有存活的对象,然后让所有存活的对象向一端移动,并清理掉末端边界以外的内存。该算法做不会产生内存碎片,并且无需设置两块内存互换,适用于存活率极高的场景。但其需要移动大量对象,处理效率较低。
分代收集算法(Generational Collector)
按照对象的生命周期划分不同的区域,并采用不同的垃圾分类算法。可以理解成上述算法的综合。新生代对象存活率低,适合采用复制算法;老年代存活率高,复制大量对象成本较高,且无额外空间进行分配担保,所以一般采用标记-清除算法或标记-整理算法。
分代收集算法中GC的分类如下:
GC分类
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此Minor GC会频繁执行,执行的速度一般也会比较快。
- Full GC:回收老年代和新生代,老年代对象存活时间长,因此Full GC很少执行,执行速度会比Minor GC慢得多。
- Major GC:只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集,即Full GC。
触发Full GC的条件
-
调用
System.gc():只是建议虚拟机执行Full GC,虚拟机不一定会真正去执行。 -
老年代空间不足:大对象直接进入老年代、长期存活的对象进入老年代,导致老年代空间不足时,会触发Full GC。解决方法:避免创建过大的对象或者增大新生代晋升老年代的年龄。
-
JDK1.7及之前永久代空间不足:永久代存放的是一些类信息、常量和静态变量等数据,当系统中加载的类、反射的类和调用方法较多时,永久代空间不足就会触发Full GC。所以JDK1.8用元空间取代永久代可以降低Full GC的频率,减少GC负担。
-
空间分配担保失败:使用复制算法的Minor GC需要老年代的内存空间作担保,如果担保失败会执行一次Full GC。
-
Concurrent Mode Failure:执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足,便会报Concurrent Mode Failure 错误,并触发Full GC。
空间分配担保
空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。
在发生Minor GC之前,虚拟机先检查老年代最大可用的连续空间否大于新生代所有对象总空间,如果大于,那Minor GC可以确认是安全的。如果不大于,虚拟机会查看HandlePromotionFailure的值是否允许担保失败发生,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC;如果小于,或者HandlePromotionFailure的值不允许冒险,那么就要进行一次Full GC。
Stop-the-world
Stop-the-world指JVM由于要执行GC而停止了应用程序的执行,它在任何一种GC算法中都会发生。多数GC优化是通过减少Stop-the-world发生的时间来提高程序性能。
Safepoint
Safepoint即垃圾收集器中的安全点,用于分析过程中对象引用关系不会发生变化的点。一般出现在方法调用、循环跳转和异常跳转等位置。安全点的数量选取要适中,选取太少会让GC等待太久,选取太多会增加程序运行负荷。
常见的垃圾收集器
前置知识:JVM运行模式。
JVM运行模式有以下两种:
- Server:Server模式采用重量级JVM,优化更多,启动慢但稳定后效率高。
- Client:Client模式采用轻量级JVM,启动的速度快。
我们先来了解一下新生代垃圾收集器。
Serial收集器
用于新生代垃圾回收,采用复制算法,单线程收集,必须暂停所有工作线程。由于没有线程交互的开销,简单高效,是Client模式下默认的新生代垃圾收集器。
参数:-XX:+UseSerialGC
ParNew收集器
用于新生代垃圾回收,采用复制算法,多线程收集,其他特点与Serial收集器基本相同。ParNew收集器在单核CPU的执行效率不如Serial收集器,因为存在线程交互带来的开销,其在多核心CPU上执行才有优势。
参数:-XX:+UseParNewGC
Parallel Scavenge收集器
用于新生代垃圾回收,采用复制算法,也是多线程收集。Parallel Scavenge收集器更注重系统的吞吐量,ParNew收集器更注重减少用户线程停顿时间。Parallel Scavenge是Server模式下默认的新生代垃圾收集器。
参数:-XX:+UseParallelGC
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
接下来是老年代垃圾收集器。
Serial Old收集器
Serial Old用于老年代垃圾回收,采用标记-整理算法。它是Serial收集器的老年代版本,除了采用的算法不同,其余特点与Serial收集器基本相同。Serial Old收集器是Client模式下默认的老年代垃圾收集器。在Parallel Old收集器诞生前,它与Parallel Scavenge收集器搭配使用。在CMS收集器发生Concurrent Mode Failure时,也会使用Serial Old收集器。
参数:-XX:+UseSerialOldGC
Parallel Old收集器
Parallel Old收集器用于老年代垃圾回收,采用标记-整理算法。Parallel Scavenge收集器的老年代版本,多线程,注重吞吐量。
参数:-XX:+UseParallelOldGC
CMS收集器
CMS(Concurrent Mark Sweep)收集器用于老年代垃圾回收,采用标记-清除算法,收集器线程可以和用户线程并发工作。CMS适合使用在对用户体验要求较高以及JVM中存在较多存活时间较长的对象的场景。它的垃圾回收过程分为以下六步:
- 初始标记:stop-the-world仅标记与GC Roots直接关联的对象,速度很快。
- 并发标记:进行GC Roots追溯标记,它在整个回收过程中耗时最长。由于并发标记的线程与应用程序的线程并发执行,所以用户不会感到停顿。
- 并发预清理:并发预清理会查找执行并发标记阶段从新生代晋升到老年代的对象。
- 重新标记:会暂停虚拟机,扫描CMS堆中的剩余对象,相对较慢。
- 并发清理:清理垃圾对象,程序不会停顿。
- 并发重置:重置CMS收集器的数据结构,等待下一次垃圾回收。
这6个步骤中,初始标记和重新标记需要短暂地stop-the-world。
CMS收集器的缺点:
-
吞吐量低,降低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高。
-
无法处理浮动垃圾。浮动垃圾是在并发清除时,由于用户线程继续运行而产生的垃圾。这部分垃圾只能等到下一次GC才能进行回收,浮动垃圾存在需要预留一部分内存,如果预留的内存不够存放浮动垃圾,就会出现Concurrent Mode Failure,这时虚拟机将临时启用Serial Old 收集器来替代CMS收集器。
-
采用的是标记-清除算法,会存在内存碎片,所以可能导致没有足够大的连续空间来分配大对象,不得不提前触发一次Full GC。
G1收集器
G1收集器(Garbage First)采用多线程收集,且可以和用户线程并发进行。它既可回收新生代,也可回收老年代,采用的是复制+标记-整理算法,不会产生内存碎片。使用G1收集器可以实现可预测的停顿,它能够让使用者指定在一个长度为M 毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒。
G1收集器将整个Java堆内存划分为多个大小相等的Region,新生代和老年代不再物理隔离。通过记录每个Region垃圾回收时间以及回收所获得的空间,维护一个优先列表,每次可根据允许收集的时间,优先回收价值最大的Region。而且每个Region中还有一个Remembered Set,用来记录该Region中对象的引用对象所在的Region,在做可达性分析的时候就可以避免全堆扫描。
G1收集器工作流程包含以下四个步骤:
- 初始标记:和CMS收集器类似。
- 并发标记:和CMS收集器类似。
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,这阶段需要停顿线程。
- 筛选回收:会对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间,来回收价值最大的一部分Region。
补充
JDK11中还有一种ZGC,它与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法。不过 ZGC 对该算法做了重大改进,可以实现非常低的线程停顿时间,它通过使用着色指针和读屏障来实现。感兴趣的同学可以自行查阅相关资料。
面试题
Object的finalize()方法的作用是否与C++的析构函数作用相同?
答:与C++的析构函数不同,析构函数调用时间确定,而finalize()方法的调用时间是不确定的。
进阶:当对象进行可达性分析时发现没有和GC Roots相连接会判断是否使用了finalize()方法。如果对象覆盖了finalize()方法,并且没使用过这个方法,对象就会被放在F-Queue中,然后稍后由JVM创建一个低优先级的finalize线程去执行触发finalize()方法。由于finalize线程优先级比较低,所以finalize()方法执行随时可能被终止。finalize()方法是为对象创建最后一次重生的机会,如果在finalize()方法中对象又再次和GC Roots相连,就不会被垃圾回收器回收。
此处我们通过一个例子直观理解:
package com.yeliheng.gc;
public class Finalization {
public static Finalization finalization;
@Override
protected void finalize() throws Throwable {
System.out.println("Finalized");
finalization = this;
}
public static void main(String[] args) {
Finalization f = new Finalization();
//第一次打印
System.out.println("1: " + f);
//将f置为空
f = null;
System.gc();
// 第二次打印
System.out.println("2: " + f);
//调用
System.out.println(f.finalization);
}
}
运行以上demo会得到如下输出:
1: com.yeliheng.gc.Finalization@29453f44
2: null
null
Finalized
Process finished with exit code 0
我们会发现,最后调用f.finalization时,finalization = this;的效果并没有显现出来。这就体现了finalize在gc调用时间上的不确定性。
由于finalize方法运行的不确定性较大,无法保证各对象的调用顺序,同时运行代价也是相当高昂的,故不建议在生产环境中使用这个方法!
Java中的强引用,软引用,弱引用,虚引用了解吗?
强引用:是最普遍的引用,JVM宁愿抛出OutOfMemoryError终止程序也不会回收具有强应用的对象,通过将对象设置为null来弱化引用,可以使其被回收。强
Object obj = new Object(); // 强引用
软引用:对象处在有用但非必须的状态,只有当内存空间不足时,GC会回收该引用的对象的内存,可以用来实现内存敏感的高速缓存,不用担心OutOfMemory的问题,而且内存一般也是充足的,引用的对象会一直存在方便复用,可以和引用队列联合使用。
String str = new String(“abc”);
SoftReference<String> softRef = new SoftReference<String>(str);
弱引用:非必须的对象,比软引用更弱一些。无论内存是否充足,GC都会回收弱引用对象。另外由于GC线程优先级比较低,被及时回收的概率也不大。适用于引用偶尔被使用且不影响垃圾回收的对象,可以和引用队列联合使用。
String str = new String(“abc”);
WeakReference<String> weakRef = new WeakReference <String>(str);
虚引用:不会决定对象的生命周期,任何时候都可能被垃圾收集器回收。主要作用是跟踪对象被垃圾收集器回收的活动,起哨兵作用。必须和引用队列ReferenceQueue联合使用。
String str = new String(“abc”);
ReferenceQueue queue = new ReferenceQueue();
PhantomReference ref = new PhantomReference(str, queue);
引用的级别由高到低分别为:强引用 -> 软引用 -> 弱引用 -> 虚引用
下面我用一个表格总结引用:
| 引用类型 | 被垃圾回收时间 | 作用 | 生命周期 |
|---|---|---|---|
| 强引用 | 从来不会回收 | 对象的一般状态 | JVM停止运行时终止 |
| 软引用 | 在内存不足时回收 | 对象缓存 | 内存不足时终止 |
| 弱引用 | 在垃圾回收时回收 | 对象缓存 | gc运行后终止 |
| 虚引用 | 任何时候都有可能 | 标记、哨兵 | 无法确定 |
补充:Java中引用的类层次结构,如下图所示。

引用队列:存储关联的且被GC的软引用,弱引用以及虚引用。用处只是为了提醒程序员非强引用型变量所引用的对象已经具有不可达性了,再也不能获得堆中的那个对象了。
CMS收集器和G1收集器的区别
- 使用范围不同:CMS收集器是老年代的收集器,可以配合新生代的Serial收集器和ParNew收集器一起使用;G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用。
- stop-the-world时间不同:CMS收集器是以最小的停顿时间为目标的收集器;G1收集器可预测指定垃圾回收的停顿时间。
- CMS收集器产生的内存碎片比G1收集器多:CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片;G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
- 垃圾回收过程不同:
- CMS收集器:初始标记、并发标记、重新标记、并发清除;
- G1收集器:初始标记、并发标记、最终标记、筛选回收;