JVM垃圾回收
对象内存回收
什么是垃圾回收
就是清理无效对象,避免一直占用内存,这种对内存资源的管理就是垃圾回收。
引用计数法
引用计数法采用的原理如下:
- 引用计数法在对象头处维护一个
counter
,每增加一次对该对象的引用计数器自加 1 - 如果对该对象的引用失联,则计数器自减 1
- 当
counter
为 0 时,表明该对象已经被废弃,是垃圾对象,可以被 GC 回收
引用计数法的优缺点:
优点🐱👓
- 实时性高,无需等到内存不够才开始回收,运行时根据对象的计数器判断是否为 0,为 0 则可以直接回收
- 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报
outofmember
错误 - 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象
缺点🐤
- 每次对象被引用都需要去更新计数器,有一点时间开销
- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计
- 无法解决循环引用问题(最大缺点)可能造成循环引用无法回收的情况
这种方式一方面无法区分强、软、弱、虚引用类别。
可达性分析算法
为了解决循环引用问题,在 Java 中采取了可达性分析法。该方法的基本思想是通过一系列的 “GC Roots”
对象作为起点进行搜索。
Java 并不采用引用计数法来判断对象是否已 “死”
,而采用 “可达性分析”
来判断对象是否存活。
可达性分析算法通过一系列 GC Roots 对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。
当一个对象到 GC Roots 没有任何引用链相连,则此对象不可用,被判断为不可达的对象。
可作为 GC Roots 的对象包括如下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象,虚拟机栈中存放的是以栈帧的形式存储的方法,因此 “虚拟机栈中引用的对象” 可以理解为待执行的方法中的局部变量所引用的对象。
- 方法区中类静态属性引用的对象,静态变量指的是
static
修饰的类的成员变量。 - 方法区中常量引用的对象,常量指的是
final
修饰的变量。 - 本地方法栈中 JNI(即 Native 方法)引用的对象,本地方法指的是用其他语言实现的方法。
基本思路:
- 通过一系列的 ‘’GC Roots‘’ 对象作为起点进行搜索
- 如果在 ‘’GC Roots‘’ 和对象之间没有可达的路径(引用链),则称该对象是不可达的,被判定不可达的对象不一定会成为可回收对象
- 被判定不可达的对象至少要经历两次标记过程,如果在这两次标记过程中扔没有逃脱成为可回收对象的可能性,则基本就成为可回收的对象了
两次标记流程
即使在可达性分析算法中对象到 GC Roots 的应用链不可达,并不是立即被回收。
引用队列 F-QUEUE
- 当垃圾回收器准备回收一个对象时,如果发现它还有引用
- 那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去
- 程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施
/**
* @author BNTang
*/
public class Test {
public static void main(String[] args) throws Exception {
ReferenceQueue<String> srq = new ReferenceQueue<>();
String str = new String("BNTang");
WeakReference<String> wr = new WeakReference<>(str, srq);
str = null;
// 是否在引用队列当中
System.out.println(wr.isEnqueued());
// 强制GC
System.gc();
Thread.sleep(300);
// 是否在引用队列当中
System.out.println(wr.isEnqueued());
System.out.println(srq.poll());
// 强制GC
System.gc();
System.out.println(srq.poll());
}
}
中间至少要经历再次标记的过程,第一次标记并进行筛选,此对象是否有必要执行 finalize()
方法,当对象没有覆盖 finalize()
方法,或者 finalize()
方法被虚拟机调用过,虚拟机将这两种情况都视为 ”没有必要执行“,对象被虚拟机回收。第二次标记,如果该对象被判定为有必要执行 finalize()
方法,那么这个对象会被放置到一个名为 F-Queue
的队列中,并在稍后由虚拟机自行建立的优先级比较低的线程 Finalizer
去执行。这里的 ‘执行’ 是虚拟机会触发 finalize()
方法。finalize() 方法是对象逃离死亡的最后一次自救,稍后 GC 将对 F-Queue
进行小规模的标记,如果对象重新与应用链上的对象建立联系,第二次标记会将他移除 “即将回收” 的集合。
引用类型
强引用(Strong Reference)
强引用就是程序代码中普遍存在的,类似 “Object obj = new Object()”
的引用,只要强引用还在,垃圾回收器永远不会回收掉被引用的对象。强引用是 Java 默认支持的一种操作模式,也就是说在引用的处理其间,即使出现有 GC,即使内存不足,该引用的数据也不会被回收。
/**
* @author BNTang
*/
public class Test {
public static void main(String[] args) {
Object oa = new Object();
Object ob = oa;
oa = null;
System.gc();
System.out.println(ob);
}
}
软引用(Soft Reference)
软引用是描述一些还有用,但并非必须的对象。软引用指的是当内存不足的时候才进行 GC 的空间释放,但是如果要想使用软引用必须单独使用特殊的处理类,java.lang.ref.SoftReference
。
构造方法:
public SoftReference(T reference);
取得引用的数据:public T get();
软引用与强引用相比,最大的特点在于:软引用中保存的内容如果在内存富裕的时候会继续保留,内存不足会作为第一批的丢弃者进行垃圾空间的释放。在开发中,可以利用软引用可以实现高速缓存组件。缓存处理好了,你的程序写的才能达到高性能的第一步。
/**
* @author BNTang
*/
public class Test {
public static void main(String[] args) {
String str = new String("BNTang");
SoftReference<String> sr = new SoftReference<>(str);
str = null;
System.gc();
// 虽然调用了GC,但是内存空间还有,所以不会释放
System.out.println(sr.get());
}
}
弱引用(Weak Reference)
不管内存是否紧张,只要一出现 GC 处理,则立即回收,弱引用最大的特点就是一旦发生了 GC 的处理操作,则保存的引用内容将立即释放。弱引用使用 “WeakReference”
类来完成。
/**
* @author BNTang
*/
public class Test {
public static void main(String[] args) {
String str = new String("BNTang");
WeakReference<String> wr = new WeakReference<>(str);
str = null;
System.out.println(wr.get());
// 当执行了GC时,对象就会被立即释放
System.gc();
System.out.println(wr.get());
}
}
/**
* @author BNTang
*/
public class Test {
public static void main(String[] args) {
WeakHashMap<String, String> whm = new WeakHashMap<>();
whm.put(new String("name"), new String("0001"));
System.out.println(whm);
System.gc();
System.out.println(whm);
}
}
虚引用(PhantomReference)
虚引用也称为 幽灵引用
或者 幻影引用
,与其他几种引用都不同,虚引用并不会决定对象的生命周期。幽灵引用指的是什么都不保存,但是又看起来像是保存了一样。这个的处理类 java.lang.ref.PhantomReference
几乎不用。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
垃圾回收算法
标记-清除算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。
标记-清除算法分为两个阶段:
- 标记阶段,标记阶段的任务是标记出所有需要被回收的对象
- 清除阶段,清除阶段就是回收被标记的对象所占用的空间
优缺点:
- 实现起来比较容易
- 但是有一个比较严重的问题就是容易产生
内存碎片
- 碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作
标记-复制算法
特点:
- 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块
- 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面
- 然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题
优缺点:
- 这种算法虽然实现简单,运行高效且不容易产生内存碎片
- 但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半
- Copying 算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么 Copying 算法的效率将会大大降低
标记-整理算法
特点:
- 标记阶段和标记清除是一样
- 但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存
分代收集
分代收集理论
虚拟机的垃圾收集都采用分代收集算法,一般将 Java 堆分为 新生代
和 老年代
,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。在新生代中,每次收集都会大批对象死去,每次回收后存活的少量对象将会逐步晋升为老年代中存放,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集,老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择 “标记-清除”
或 “标记-整理”
算法进行垃圾收集,根据不同分代的特点,选择对应的垃圾回收算法。
分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation)老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
现状:
目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照 1:1
的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将 Eden 和 Survivor 中还存活的对象复制到另一块 Survivor 空间中,然后清理掉 Eden 和刚才使用过的 Survivor 空间。
Stop-the-world
它是指 JVM 由于要执行 GC 而停止了应用程序的执行,并且这种情形会在任何一种 GC 算法中发生。当 Stop-the-world 发生时,除了 GC 的线程以外,其他的线程均处于等待的状态,直到 GC 任务完成。很多 GC 优化都是通过减少 Stop-the-world 的时间来提高程序的性能。
Safe-point
程序执行时并非在所有地方都能停顿下来开始 GC,只有在某些特定的位置才可以,这些特定的位置被称为 安全点(Safe-point)
在使用 GC roots 分析可达性时,引用关系不会发生改变的点就是安全点。
常用的安全点有如下几种:
- 循环的末尾
- 方法返回前
- 调用方法的 call 之后
- 抛出异常的位置
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具