JVM的垃圾回收处理

本文最后更新于 2025年8月2日 晚上

内存,对象与垃圾回收

在解释器语言实现中,往往是通过虚拟机的方式,先向系统申请一块较大内存,再从其中逐渐开辟内存空间,创建所需要的结构体。这种方式带来了几个特性 :

  1. 通过托管内存-结构创建的方式,所有创建的对象对虚拟机来讲都是可以进行管理的。
  2. 托管内存本身也变为了可管理的,这样执行命令时虚拟机就可以对内存进行监控。
  3. 因为对象是可以被查询(通过类型定义结构)和可管理的(绝大部分实例资源都创建在堆结构下),所以就可以对这些对象的生命周期进行管理。

垃圾回收

垃圾回收需要考虑的阶段 :

哪些内存需要回收?

什么时候回收?

如何回收?

引用计数法

引用计数法 通过在对象中添加一个引用计数器,每当有一个地方引用它时,计数器直接就加一,当引用失效时,计数器值就减一,任何时刻计数器为零的对象 就会发垃圾回收过程。

主要缺点

  1. 绝大部分的对象都是在创建后很短时间内就被废弃了,但是对于每个对象都需要进行计数过程,增加了系统的CPU资源消耗
  2. 使用引用计数法的 垃圾回收会存在相互引用的问题,比如A中存在对B的引用,B中又存在对A进行了引用,则A当前的引用计数(A被B引用的计数+a对A的原本引用 = 2,),B的引用计数(B被A引用的计数+b对B的原本引用=2)),这时将a,b的引用分别指向别的对象,这时 A对象和B对象已经无法再从外部对其进行访问了,但是引用计数仍不为0,导致无法被销毁
  3. 无法处理复杂对象结构中被多方引用但整体不可达的情况(即仅靠引用计数难以识别全局不可达)

可达性分析算法

可达性分析算法 通过一系列称为GC Roots的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,如果一个对象到GC Roots间没有任何引用链相连,则证明此对象是不可用的。

Java中的 “GC Roots” 对象

  1. 在虚拟机栈中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量等。
  2. 在方法区中类静态属性引用的对象,如java类的引用类型静态变量
  3. 在方法区中常量引用的对象,如字符串常量池里的引用
  4. 在本地方法栈中引用的对象
  5. Java虚拟机内部的引用
  6. 同步锁持有的对象

相比引用计数法,可达性分析能更准确识别死对象,但需要停顿执行以保证一致性。

finalize

对死亡对象的标记过程:

  1. 对象 不可达 → 被标记
  2. 检查对象是否需要执行finalize()方法

需要执行finalize方法的对象会被加入到F-Queue队列中等待执行finalize方法。

如果没有被重新引用,或者不需要执行finalize方法,则对象将要被回收了

方法区的回收

在方法区主要针对 不再使用的类型和废弃的常量进行回收。

对于常量: 当已经没有任何字符串对象引用常量池中的某个常量,这个常量就可以作为垃圾被回收

对于类型 :

1.该类所有的实例已经被回收,Java堆中不存在任何该类及其派生子类的实例

2.加载该类的类加载器已经被回收。

3.该类的Class对象没有被任何地方引用,无法在任何地方通过反射访问该类的方法

满足以上条件,就可以尝试对 常量和类型进行回收。

垃圾收集算法

为了更高效地释放托管内存,我们需要采用一套合适的垃圾收集算法。根据不同的识别策略,垃圾收集算法可以分为引用计数与追踪式两大类:

1.引用计数收集 - 即时收集 - 对象的计数为0时触发垃圾回收

2.追踪式收集 - 由解释器选择时机进行触发 - 对不再使用的对象进行标记 - 择机触发垃圾回收

Java一般使用的是以追踪式收集为基础的回收算法

分代收集理论

  1. 弱分代假说 : 绝大多数对象都是朝生夕灭的
  2. 强分代假说 :熬过越多次垃圾收集过程的对象就越难以消亡

基于以上两条理论,就可以使用分区的方式 对处于不同生命周期的对象进行管理,并筛选出其中的更可能一直存在的对象,减少对它的访问,来提升整体的GC效率。

当然,同样存在存活时间较长的对象对村存活时间较短对象的跨代引用,这时可以根据经验

  1. 跨代引用假说:跨代引用对于同代引用来说仅占极少数

因为如果长期存在的引用,必然会随着进入老年代的引用对象一起进入老年代。

所以可以不必因为少量跨代引用去频繁扫描老年代,只需要使用记忆集,将存在跨代引用的老年代对象标记出来只对该部分的对象进行扫描。

GC 策略

部分收集(Partial GC)目标不是完整收集整个Java堆的垃圾收集。其中又分为

新生代收集(Minor GC/ Young GC) 指目标只是新生代的垃圾收集

老年代收集(Major GC/Old GC)指目标只是老年代的垃圾收集 (CMS收集器)

混合收集 (Mixed GC)指目标是收集整个新生代以及部分老年代的垃圾收集(G1收集器)

整堆收集(Full GC)收集整个Java堆和方法区的垃圾收集

标记清除算法

首先标记出所有需要回收的对象,在标记完成之后,统一回收掉所有被标记的对象。

或者统一回收掉所以没有被标记的对象。标记过程就是判断对象是否属于垃圾的过程。

缺点 :

1.执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作。导致整体执行时间过长

2.内存碎片化问题。因为在创建对象时是指针碰撞或者使用空闲列表查询内存申请创建的对象,对象的生命周期并不在创建对象的内存排布策略之中,在多次创建/销毁之后,就会导致在创建较大对象时没有连续内存而触发GC。

标记复制算法

标记复制算法通过半区复制的方式完成垃圾回收过程,当用于存储的半区被用完了,触发GC过程,将存活的对象复制到另外一半上。因为 大多数对象都是朝生夕灭的,所以在复制开销还是可以接收的。同时解决了内存碎片化问题。但是缺点就是有一半的内存是不可用的。

增强

将新生代 分为3个区域。Eden / Survivor0 / Survivor1 。 内存使用比例为8:1:1, 通过观察在绝大多数情况下98%的对象都会在第一轮收集中被回收。这样当程序创建对象时,先在Eden/ Survivor0中创建对象,当发生GC时,将Eden / Survivo0 中的存活对象写入到Servivor1中,清理Eden/Suervivo0的空间。而当出现了存活对象超过Survivor大小时,就尝试从其他区域获取内存进行分配担保。

标记整理算法

对于新生代可以通过在内存中分区来完成对象的垃圾收集和整理过程。但是对于老年代这种存活对象较多的区域,移动所有对象是一种成本极高的操作。但是如果不移动就会因为碎片化问题需要考虑更加复杂的内存分配策略。

关注吞吐量的 会使用 标记整理算法 → 能够更好的腾出空间,减少因为没有较大内存区域而频发触发GC

关注时延的 则会使用 标记清除算法 → GC停顿时间更短,能够更快地进行内存的申请

也可以 高频率使用标记清除算法 ,当碎片过多时使用标记整理算法

HotSpot 算法实现细节

  1. 为了保证对对象引用的准确性,所以在垃圾收集期间,必然会暂停用户线程。
  2. HotSpot通过一个Oopmap的结构在类加载期间完成对某个对象中的引用对象的偏移量和类型进行收集,在GC过程中直接从这个结构中使用偏移和类型最终获取到对象进行标记,而不用完全从所有的GC Roots开始查找。这里是针对栈中所使用的引用对象信息进行的优化。
  3. 安全点与安全区域
    对 每次OopMap的长期存储 不符合系统整体的内存使用要求,所以只有在某些节点上触发了GC时会生成当前栈调用的OopMap 结构,所以也要求线程都要执行到安全点才能暂停。
    通过主动式中断完成在安全点暂停,当需要执行GC时,将所有线程中的标志位设置为true,一旦为true 后,线程会主动在最近的安全点将自己挂起,
    当 线程因为指定或者获取不到时间片而无法到达安全点时,则在这些地方将自己标识为进入了安全区域,当线程要离开时,需要检查虚拟机算法已经完成了 需要暂停用户线程的阶段,完成了则可以继续执行,没有完成,则必须一直等待可以收到离开安全区域的信号为止。

总结

垃圾回收并不仅仅是回收内存,更重要的是在保障程序正确性的同时,尽可能提升执行效率。不同语言、不同场景下,所采用的回收算法也存在显著差异。理解其核心机制有助于我们写出更高效、稳定的代码。各种垃圾收集器的具体特性暂不在此处进行讨论。


JVM的垃圾回收处理
http://gadoid.io/2025/07/31/JVM的垃圾回收处理/
作者
Codfish
发布于
2025年7月31日
许可协议