JVM内存结构中的堆存在垃圾回收机制。
如何判断对象可以被回收
引用计数法
只要一个对象被其他对象引用,就让他引用计数+1,如果别的对象不再引用他了,就让他的引用计数-1,只要引用计数变为0,就代表没有别的对象引用他了,就可以回收这个对象。
【缺点
】:无法解决循环引用的问题,A、B对象的引用计数都为1,他们互相引用。
可达性分析算法
扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象;如果找不到,表示可以回收。
五种引用
强引用
必须C对象
和B对象
没有引用A1对象,A1对象
才能被垃圾回收
- 最常见的引用类型
- 只要强引用存在,垃圾收集器就永远不会回收被引用的对象
- 即使内存不足时,JVM抛出OOM错误也不会回收强引用对象
Object obj = new Object(); // 强引用
【
适用场景
】:普通对象创建和使用、需要长期存在的对象
软引用
如果B对象不再引用A2对象,并且发生垃圾回收时内存不够
,就会把A2对象也回收掉(如果软引用配合了引用队列,则软引用对象会进入引用队列中)
- 内存充足时不会被回收
- 内存不足时会被垃圾收集器回收
- 适合用于实现内存敏感的缓存
// SoftReference --> Object
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get(); // 获取引用对象
【
适用场景
】:实现内存缓存、图片缓存可能占用大量内存但可以重新创建的对象
/**
* 软引用配合引用队列
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
* @author xiaolin03
* @date 2025/5/9
*/
public class Demo01 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列,完成软引用的清理
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for(int i = 0; i < 5; i++) {
// ref关联了引用队列,当软引用所关联的byte[]被回收时,软引用自己就会被加入到queue中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue); // byte数组存储图片资源
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 从队列中获取无用的软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while(poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("========");
for(SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
弱引用
如果B对象不再引用A3对象,只要发生垃圾回收,不管内存是否充足
,都会回收A3对象(如果软引用配合了引用队列,则弱引用A2的对象会进入引用队列中)
- 比软引用更弱
- 只要发生垃圾收集,无论内存是否充足都会被回收
- 常用于实现规范化映射(如WeakHashMap)
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get(); // 可能返回null
【
适用场景
】:实现规范化映射、监控对象是否被回收、临时性数据存储
虚引用
A4对象被垃圾回收时,虚引用对象就会进入垃圾回收队列,从而间接地由一个线程调用虚引用对象的方法,调用Unsafe.freeMemory,进而释放直接内存。
- 最弱的引用类型
- 无法通过虚引用获取对象实例(get()总是返回null)
- 主要用于跟踪对象被垃圾回收的活动
- 必须与ReferenceQueue配合使用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// phantomRef.get() 总是返回null
【
适用场景
】:精确控制对象回收后的资源清理、实现比finalize更灵活的资源释放机制
终结器引用(不推荐)
当A4被垃圾回收时,会将终结器引用对象加入引用队列中(此时A4对象还没被垃圾回收),由一个优先级很低的线程,在某些时机查看是否有终结器引用对象,根据引用队列中的终结器引用对象,调用A4对象的垃圾回收方法(fanallize())
这样会导致A4对象迟迟无法被释放。
- JVM内部使用的特殊引用
- 用于实现对象的finalize()方法
- 不推荐在应用代码中使用
- 可能导致性能问题和内存泄漏
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakRef = new WeakReference<>(new Object(), queue);
// 当对象被回收后
Reference<?> ref = queue.poll(); // 获取被回收的引用对象
所有引用类型(除了强引用)都可以关联一个引用队列。当引用对象被垃圾回收时,引用对象本身会被放入引用队列,可以通过轮询队列来了解哪些对象被回收了。
垃圾回收算法
标记清除算法
原理:
- 标记阶段:从GC Root开始遍历,标记所有可达对象
- 清除阶段:回收未被标记的对象占用的内存
优点:实现简单、不需要移动对象
缺点:
- 产生内存碎片导致分配大对象可能会失败;
- 效率会随堆大小的增加而降低
标记整理算法
原理:
- 标记阶段:从GC Root开始遍历,标记所有可达对象
- 整理阶段:将所有存活的对象向内存的一端移动
- 清理边界外的内存
优点:无内存碎片;内存利用率高
缺点:
- 移动对象开销大;
- 需要暂停用户线程(Stop The World)
复制算法
原理:
- 将内存分为大小相等的两块(From和To空间)
- 只使用其中一块(From空间)
- GC时把存活对象复制到另一块(To空间)
- 清空原来的From空间
- 交换From和To空间
优点:无内存碎片;分配内存效率高;回收效率高
缺点:
- 内存利用率只有50%
- 对象存活率高时效率下降
- 需要额外的内存空间
JVM是结合不同场景来使用不同的垃圾回收机制
分代垃圾回收机制
【本质
】针对不同的区域采用不同的垃圾回收算法,这样就能更有效地对虚拟机中的垃圾进行管理
- 新生代(伊甸园 + 幸存区(from + to))
- 伊甸园:新对象分配区
- 幸存区(from/to):存放Minor GC后存活的对象
- 老年代:存放长期存活的对象
- 新生代:复制算法(对象存活率低)
- 老年代:标记 - 清除算法 or 标记 - 整理算法
步骤:
- 对象首先分配在伊甸园区
- 新生代空间不足时,会触发
minor gc
,伊甸园和幸存区from存活的对象使用copy赋值到to中,存活的对象计数器+1,并交换from区域 和 to区域 - minior gc会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值(15次 - 4bit),会晋升至老年代。
- 当老年代空间不足,会先尝试触发minior gc,如果之后空间仍不足,那么触发
full gc
(full gc也会引发stop the world,STW的时间更长) - 如果full gc后,新生代和老年代的空间都放不下,就会内存溢出。
垃圾回收器
串行
- 单线程回收,全程STW
- 适合堆内存较小,适合个人电脑
关键JVM参数:
-XX:+UseSerialGC = Serial + SerialOld # 开启垃圾回收
【
代表回收器
】:
- Serial:新生代——复制算法
- SerialOld:老年代——标记整理算法
吞吐量优先
- 多线程并行回收
- 适合堆内存较大的场景,需要多核CPU
- 让
单位时间内STW的时间
最短(0.2 + 0.2 = 0.4) - 高吞吐量,适合长时间运行的CPU密集型任务
- 单次GC停顿时间可能较长
关键JVM参数:
-XX:+UseParallelGC # 启用 Parallel Scavenge(新生代)
-XX:+UseParallelOldGC # 启用 Parallel Old(老年代)
-XX:GCTimeRatio=99 # 吞吐量目标(默认 99,即 GC 时间不超过 1%(1/radio + 1))
JDK8默认开启
垃圾回收线程默认和CPU核数相同
【代表回收器
】:
- Parallel Scavenge:新生代——复制算法
- Parallel Old:老年代——标记-整理算法
响应时间优先
- 多线程
- 适合堆内存较大的场景,需要多核CPU
- 尽可能让
单次STW的时间
最短(0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5) - 适用于:Web服务、实时交易系统、对低延迟敏感的应用
初始标记
(很快):STW,只标记GC Root
并发标记
(和用户线程并发执行):不会STW,标记出剩余的垃圾对象
重新标记
:STW,因为在并发标记的过程中,用户线程也在运行,有可能会产生新的垃圾对象,所以这里又需要重新标记一次
并发清理
:清理所有的垃圾对象
关键JVM参数:
-XX:+UseConcMarkSweepGC # 启用 CMS(JDK 14 前)
-XX:+UseG1GC # 启用 G1
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间(毫秒)
【
代表回收器
】
- CMS:并发标记清除,减少STW时间,但是存在内存碎片问题(JDK 14 后删除)
- G1:分Region回收,可以预测停顿时间
- ZGC:全并发回收,停顿时间<10ms
G1(Garbage First)
JDK9开始G1是默认的垃圾回收器(取代了CMS)
适用场景:
- 同时注重吞吐量和低延迟,默认暂停目标是200ms
- 超大堆内存,会将堆划分成多个大小相等的区域
- 整体上是
标记-整理算法
,两个区域之间是复制算法
垃圾回收阶段
- 新生代垃圾收集
- 新生代垃圾收集 + 并发标记
- 混合垃圾收集(新生代幸存区、老年代)
1. 新生代垃圾收集
- 刚创建的对象会进入伊甸园区(绿色区域)
- 如果伊甸园满了,就会触发一次垃圾回收【STW】,把伊甸园里的对象放入幸存区(蓝色区域)
- 如果幸存区的区域也满了 或 幸存区的对象计数器达到15,那么又会触发一次垃圾回收,将幸存区的对象放入老年代(橙色区域)
2. 新生代的垃圾回收和并发标记阶段
- 在Young GC时会进行GC Root的初始标记
- 老年代占用堆空间比例达到阈值时,会进行并发标记(不会STW)
3. 混合收集
会对E、S、O进行全面垃圾回收
- 最终标记会STW
- 拷贝存活会STW
跨代引用问题
跨代引用是指新生代对象被老年代对象引用,或者老年代对象被新生代对象引用的情况。这种引用关系会导致垃圾回收时需要对整个堆进行扫描,严重影响GC效率。
- 将堆划分为多个卡页(Card Page),通常512字节一个卡
- 用字节数组表示卡表,每个元素对应一个卡页
- 当老年代对象引用新生代对象时,对应卡被标记为“脏卡”
垃圾回收时只扫描脏卡对应的内存区域
垃圾回收调优
最快的GC是不发生GC
查看FullGC前后的内存占用,考虑:
- 数据是否太多?(直接查一张大表)
- 数据表示是否太臃肿?
- 每次查询的时候,把它关联的所有查出来了,而不是用到哪个查哪个
- 对象大小:Integer16、int4
- 是否存在内存泄漏?
- 定义静态map集合对象,不断地往里边插入数据,但是又不移除(解决:软引用、弱引用、第三方缓存实现)
新生代调优
新生代特点:
- 所有的new操作的内存分配非常廉价
- 死亡对象的回收代价是0
- 大部分对象都是用过即死
- Minor GC的时间远低于Full GC
新生代如果设置太小,会导致空间经常容易不足,启动Minor GC;如果设置的太大,会导致清理新生代内存时耗费的时间较长。
- 新生代设置的大小需要可以容纳所有的【并发量 * (请求 - 响应)】的数据
- 幸存区设置的大小大到能够保留【当前活跃对象 + 需要晋升的对象】
老年代调优(以CMS为例)
- 老年代内存越大越好
- 一般是先调试新生代的垃圾回收,实在不行,才看老年代的调优
- 先尝试不调优,如果没有Full GC,说明老年代的空间很充裕
- 观察发生Full GC时老年代的内存占用,将老年代内存预设调大(1/4~1/3)