垃圾回收策略与算法

垃圾回收策略与算法

需要关注垃圾回收的内存位置

程序计数器、java虚拟机栈、本地方法栈、栈帧的生命周期跟随线程或方法,内存在生命周期结束时回收,不需考虑回收问题。

关注的内存区域:堆跟方法区[程序运行期间才知道创建哪些对象,内存分配和回收都是动态的,垃圾收集器所关注的是这部分内存]。

判断对象是否存活

若一个对象不被任何对象或变量引用,那么它就是无效对象,需要被回收。

  • 引用计数法

对象头,维护计数器count,被引用则计数器+1,引用失效则计数器-1。当计数器为0时,就认为该对象无效。

循环引用及多线程下计数器变更需要昂贵同步的原因,主流虚拟机都不使用该方法管理内存。

  • 可达性分析法

所有和GC Root关联的对象就是有效对象,和GC Root没有关联的对象就是无效对象。

  • GC Root
Java虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中引用的对象
方法区中常量引用的对象
方法区中类静态属性引用的对象

GC Root 不包含堆中对象所引用的对象,这样就不会有循环引用的问题。

引用的种类

  • 强引用

"Object obj = new Object()",这类引用为强引用。

强引用存在,垃圾收集器永远不会回收被引用的对象。

  • 软引用

只有JVM认为内存不足时,才会试图回收软引用指向的对象。

JVM会确保在抛出OutOfMemoryError之前,清理软引用指向的对象。

软引用通畅用来事项内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

  • 弱引用

当JVM进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

  • 虚引用

也称,幽灵引用或者幻影引用。

提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做 Post-Mortem 清理机制。

回收堆中无效对象

  • finalize() 方法

该方法称为终结器,在实例被垃圾回收器回收时触发的操作。

如果对象被判定为有必要执行 finalize() 方法,那么对象会被放入一个 F-Queue 队列中,虚拟机会以较低的优先级执行这些 finalize() 方法,但不会确保所有的 finalize() 方法都会执行结束。如果 finalize() 方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。

  • 对象重生或死亡

如果在执行 finalize() 方法时,将this赋给了某一个引用,那么该对象就重生了。

如果没有,那么就会被垃圾收集器清除。

回收方法区内存

方法区中主要清除两种垃圾
  • 废弃常量

判定废弃常量:只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。

  • 无用的类

判定无用的类:

  1. 该类的所有对象都已经被清除
  2. 加载该类的ClassLoader已经被回收
  3. 该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。[一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除]

垃圾收集算法

判定无效对象、无用类、废弃常量,之后就可以回收这些垃圾。

常见的垃圾收集算法
标记-清除算法
  • 标记过程(Mark)

遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。

  • 清除的过程(Swep)

遍历堆中所有的对象,将没有标记的对象全部清除掉。

与此同时,清除那些被标记过的对象的标记,以便下次的垃圾回收。

  • 标记-清除算法的不足之处
    • 效率问题:

标记和清除两个过程的效率都不高。

  • 空间问题:

标记清除之后,会产生大量不连续的内存碎片,碎片太多可能导致后续需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法(新生代)

复制算法,将内存按容量划分为大小相等的两块,每次只使用其中的一块。

当其中一块内存用完,需要进行垃圾收集时,就将存活着的对象复制到另一块上面,然后将原有那块内存全部清除。

  • 这种方式的优缺点:

优点:不会有内存碎片的问题。

缺点:内存缩小为原来的一半,浪费空间。

为了解决空间利用率问题,可以将内存分为三块:Eden、From Survivor、 To Survivor,比例是8:1:1,每次使用Eden和其中一块Survivor。回收时,将Eden和Survivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用的Survivor空间。这样只有10%的内存被浪费。

但是无法保证每次回收只有不多于10%的对象存活,当Survivor空间不够,需要依赖其他内存(老年代)进行分配担保。

  • 分配担保
    为对象分配内存空间时,如果Eden+Survivor中空闲空间无法装下该对象,会触发MinorGC进行垃圾回收。如果MinorGC过后,依然有超过10%的存活对象,这样存活的对象直接通过分配担保机制进入老年代,然后再将新对象存入Eden区。
标记-整理算法(老年代)
  • 标记

第一阶段,与标记-清除算法一样,遍历GC Roots,然后将存活的对象标记。

  • 整理

第二阶段,移动所有存活的对象,并且按照内存地址次序依次排列,然后将末端内存地址以后得内存全部回收。

这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。

分代回收算法

根据对象存活周期不同,将java堆划分为新生代跟老年代。针对各个年代的特点采用适当的回收算法。

新生代:使用复制算法

老年代:使用标记-清除算法、标记-整理算法

总结

重点分析了垃圾回收所关注的内存区域,如何判断无效类、无效对象及废弃的常量,基于前序无效或废弃判断,使用不同的垃圾回收算法进行内存的回收管理。

垃圾回收关注的内存区域是堆内存。

如何判断对象是否存活?一般使用可达性分析法。

强引用、软引用、弱引用、虚引用等几种引用类型的对象回收策略。

回收堆中无效对象与回收方法区内存。

常见的垃圾回收算法,标记-清除算法,标记-复制算法(新生代),标记-整理算法(老年代)以及根据对象分代,混合前述算法的的分代回收算法。

posted @ 2025-07-16 15:39  biby  阅读(22)  评论(0)    收藏  举报