1. 如何判断对象可以回收
1.1 引用计数法
假设有一个对象 A,任何一个对象对 A 的引用,那么对象 A 的引用计数器 +1,当引用失败时,对象 A 的引用计数器就 -1,如果对象 A 的计数器的值为 0,就说明对象 A 没有引用了,可以被回收
这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在 Java 中并没有采用这种方式
1.2 可达性分析算法
JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
在 Java 语言中,可作为 GC Roots 的对象包含以下几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中(Native 方法)引用的对象
1.3 五种引用
1.3.1 强引用
回收条件:只有所有 GC Root 都不引用该对象时,才会回收强引用对象
如上图 B、C 对象都不引用 A1 对象且触发垃圾回收,A1 对象才会被回收
1.3.2 软引用
回收条件:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
注意软引用本身不会被清理,如果想要清理软引用,需要使用引用队列(大概思路为:查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除)
如上图如果 B 对象不再引用 A2 对象且触发了回收条件,A2 对象才会被回收
public class SoftReferenceDemo {
private static final int _4MB = 4 * 1024 * 1204;
/**
* 添加 VM 参数 -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public static void main(String[] args) {
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
// 添加软引用
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
// 关联引用队列, 当软引用所关联的 byte[] 数组被回收时, 软引用自己会加入到 queue 中
SoftReference<byte[]> softReference = new SoftReference<>(new byte[_4MB], queue);
list.add(softReference);
for (SoftReference<byte[]> reference : list) {
System.out.print(reference.get() + " ");
}
System.out.println();
}
// 先清除引用队列中的元素, 如果不移除, 那么 list 中将会存在许多为 null 的软引用对象
Reference<? extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("遍历集合中的元素");
// 看看还剩下多少
for (SoftReference<byte[]> reference : list) {
System.out.print(reference.get() + " ");
}
System.out.println();
}
}
[B@1d44bcfa
[B@1d44bcfa [B@266474c2
[GC (Allocation Failure) [PSYoungGen: 1798K->512K(6144K)] 11430K->10190K(19968K), 0.0033284 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[B@1d44bcfa [B@266474c2 [B@6f94fa3e
# GC 分配失败,再次触发 GC
[GC (Allocation Failure) --[PSYoungGen: 5553K->5553K(6144K)] 15231K->15231K(19968K), 0.0017626 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 5553K->5199K(6144K)] [ParOldGen: 9678K->9632K(13824K)] 15231K->14832K(19968K), [Metaspace: 3174K->3174K(1056768K)], 0.0036386 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) --[PSYoungGen: 5199K->5199K(6144K)] 14832K->14840K(19968K), 0.0009860 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
# 再次触发 GC 时回收对象
[Full GC (Allocation Failure) [PSYoungGen: 5199K->0K(6144K)] [ParOldGen: 9640K->366K(9728K)] 14840K->366K(15872K), [Metaspace: 3174K->3174K(1056768K)], 0.0034105 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
null null null [B@5e481248
null null null [B@5e481248 [B@66d3c617
遍历集合中的元素
[B@5e481248 [B@66d3c617
1.3.3 弱引用
回收条件:只有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象
如上图如果 B 对象不再引用 A3 对象且触发了回收条件,A3 对象才会被回收
弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference
public class WeakReferenceDemo {
private static final int _4MB = 4 * 1024 * 1204;
/**
* 添加 VM 参数 -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public static void main(String[] args) {
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
// 添加软引用
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
// 关联引用队列, 当软引用所关联的 byte[] 数组被回收时, 软引用自己会加入到 queue 中
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB], queue);
list.add(weakReference);
for (WeakReference<byte[]> reference : list) {
System.out.print(reference.get() + " ");
}
System.out.println();
}
// 先清除引用队列中的元素, 如果不移除, 那么 list 中将会存在许多为 null 的软引用对象
Reference<? extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("遍历集合中的元素");
// 看看还剩下多少
for (WeakReference<byte[]> reference : list) {
System.out.print(reference.get() + " ");
}
System.out.println();
}
}
[B@1d44bcfa
[B@1d44bcfa [B@266474c2
[GC (Allocation Failure) [PSYoungGen: 1684K->512K(6144K)] 11316K->10203K(19968K), 0.0041573 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
[B@1d44bcfa [B@266474c2 [B@6f94fa3e
[GC (Allocation Failure) [PSYoungGen: 5553K->464K(6144K)] 15245K->10155K(19968K), 0.0028516 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@1d44bcfa [B@266474c2 null [B@5e481248
[GC (Allocation Failure) [PSYoungGen: 5448K->464K(6144K)] 15140K->10155K(19968K), 0.0008054 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@1d44bcfa [B@266474c2 null null [B@66d3c617
[GC (Allocation Failure) [PSYoungGen: 5427K->496K(6144K)] 15119K->10187K(19968K), 0.0005936 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[B@1d44bcfa [B@266474c2 null null null [B@63947c6b
[GC (Allocation Failure) [PSYoungGen: 5446K->496K(6144K)] 15138K->10187K(19968K), 0.0007914 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[B@1d44bcfa [B@266474c2 null null null null [B@2b193f2d
[GC (Allocation Failure) [PSYoungGen: 5437K->480K(5120K)] 15129K->10195K(18944K), 0.0007610 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 480K->32K(5632K)] 10195K->10123K(19456K), 0.0024642 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(5632K)] [ParOldGen: 10091K->413K(7680K)] 10123K->413K(13312K), [Metaspace: 3175K->3175K(1056768K)], 0.0051050 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
null null null null null null null [B@355da254
null null null null null null null [B@355da254 [B@4dc63996
[GC (Allocation Failure) [PSYoungGen: 99K->192K(5632K)] 10144K->10237K(19456K), 0.0003960 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 192K->64K(5632K)] 10237K->10109K(19456K), 0.0005029 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 64K->0K(5632K)] [ParOldGen: 10045K->515K(8704K)] 10109K->515K(14336K), [Metaspace: 3184K->3184K(1056768K)], 0.0017239 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
null null null null null null null null null [B@d716361
遍历集合中的元素
[B@d716361
1.3.4 虚引用
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
当虚引用对象所引用的对象被回收以后,虚引用对象就会被放入引用队列中,调用虚引用的方法
虚引用的一个体现是释放直接内存所分配的内存,当引用的对象 ByteBuffer 被垃圾回收以后,虚引用对象 Cleaner 就会被放入引用队列中,然后调用 Cleaner 的 clean()
方法来释放直接内存
如上图,B 对象不再引用 ByteBuffer 对象,ByteBuffer 就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象 Cleaner 放入引用队列中,然后调用它的 clean()
方法来释放直接内存
1.3.5 终结器引用
所有的类都继承自 Object 类,Object 类有一个 finalize()
方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的 finalize()
方法,第二次 GC 时才能回收被引用对象
如上图,B 对象不再引用 A4 对象。这是终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的 finalize()
方法
2. 垃圾回收算法
2.1 标记清除
2.1.1 原理
算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象
2.1.2 优缺点
效率问题:标记和清除过程的效率都不高
空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致大对象无法分配到足够的连续内存,从而不得不提前触发 GC,甚至 Stop The World
2.2 标记整理
2.2.1 原理
标记过程仍然与 标记-清除 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
2.2.2 优缺点
优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响
2.3 复制
2.3.1 原理
1. Eden、SurvivorFrom 复制到 SurvivorTo,年龄 +1
首先,当 Eden 区满的时候会触发第一次 GC,把还活着的对象拷贝到 SurvivorFrom 区,当 Eden 区再次触发 GC 的时候会扫描 Eden 区和 From 区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到 To 区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄 +1
2. 清空 Eden、SurvivorFrom
然后,清空 Eden 和 SurvivorFrom 中的对象,也即复制之后有交换,谁空谁是 to
3. SurvivorTo、SurvivorFrom 互换
最后,SurvivorTo 和 SurvivorFrom 互换,原 SurvivorTo 成为下一次 GC 时的 SurvivorFrom 区。部分对象会在 From 和 To 区域中复制来复制去,如此交换 15 次(由 JVM 参数 MaxTenuringThreshold 决定,这个参数默认是 15),最终如果还是存活,就存入到老年代
2.3.2 优缺点
在垃圾对象多的情况下,效率较高,清理后内存无碎片
效率问题:在对象存活率较高时,复制操作次数多,效率降低
空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)
3. 分代垃圾回收
根据回收对象的特点进行选择,在 JVM 中,年轻代适合使用复制算法,老年代适合使用标记整理算法
3.1 回收流程
对象首先分配在伊甸园区域
新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW 的时间更长
3.2 相关 JVM 参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC 详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
3.3 GC 分析
大对象处理策略
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代
线程内存溢出
某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行
这是因为当一个线程抛出 OOM 异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常
4. 垃圾回收器
4.1 相关概念
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99%
4.2 串行
它为单线程环境设计并且只有一个线程进行垃圾回收,会暂停所有的用户线程(Stop-the-world)
不适合服务器环境
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
4.2.1 Serial 收集器
Serial 收集器是最基本的、发展历史最悠久的收集器
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)
4.2.2 ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本
特点:多线程、ParNew 收集器默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads
参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题
4.2.3 Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本
4.3 吞吐量优先
多个垃圾回收线程并行工作,此时用户线程是暂停的(Stop-the-world)
适用于科学计算/大数据处理等弱交互场景
4.3.1 Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器
特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)
GC自适应调节策略:Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy
参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation
)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold
)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。
4.3.2 Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本
4.4 响应时间优先
用户线程与垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程
互联网公司多用它,适用于对响应时间有要求的场景
4.4.1 CMS 收集器
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:
- 初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题
- 并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
- 并发清除:对标记的对象进行清除回收
4.5 G1 垃圾收集器
同时注重吞吐量和低延迟(响应时间)
超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域
4.5.1 常用配置参数
-XX:+UseG1GC
使用 G1 垃圾收集器-XX:G1HeapRegionSize=n
,设置的 G1 区域的大小。值是 2 的幂,范围是 1MB 到 32MB。目标是根据最小的 Java 堆大小划分出约 2048 个区域-XX:MaxGCPauseMills=n
,最大 GC 停顿时间,这是个软目标,JVM 将尽可能(但不保证)停顿小于这个时间-XX:InitiatingHeapOccupancyPercent=n
,堆占用了多少的时候就触发 GC,默认为 45-XX:ConcGCThreads=n
,并发 GC 使用的线程数-XX:G1ReservePercent=n
,设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是 10%
4.5.2 G1 垃圾回收阶段
新生代伊甸园垃圾回收 –> 内存不足,新生代回收+并发标记 –> 回收新生代伊甸园、幸存区、老年代内存 —> 新生代伊甸园垃圾回收(重新开始)
1. Young Collection
分区算法 region
- 分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间
- E:伊甸园 S:幸存区 O:老年代
2. Young Collection + CM
CM:并发标记
- 在 Young GC 时会对 GC Root 进行初始标记
- 在老年代占用堆内存的比例达到阈值时,对进行并发标记(不会STW),阈值可以根据用户来进行设定
3. Mixed Collection
会对E S O 进行全面的回收
- 最终标记
- 拷贝存活
为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存
4.5.3 Full GC
G1 在老年代内存不足时(老年代所占内存超过阈值)
- 如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
- 如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC
4.5.4 巨型对象
一个对象大于region的一半时,就称为巨型对象
G1不会对巨型对象进行拷贝
回收时被优先考虑
G1会跟踪老年代所有 incoming 引用,如果老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉
4.6 总结
参数 | 新生代垃圾收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 |
---|---|---|---|---|
-XX:+UseSerialGC | SerialGC | 复制 | SerialOldGC | 标记-整理 |
-XX:+UseParNewGC | ParNew | 复制 | SerialOldGC | 标记-整理 |
-XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel Scavenge | 复制 | Parallel Old | 标记-整理 |
-XX:+UseConcMarkSweepGC | ParNew | 复制 | CMS + Serial Old 的收集器组合 Serial Old 作为 CMS 出错的后备收集器 |
标记-清除 |
-XX:+UseG1GC | G1 整体上采用标记-整理算法 | 局部是通过复制算法,不会产生内存碎片 |