JVM垃圾收集策略与算法( 二 )


判定 finalize() 是否有必要执行
JVM 会判断此对象是否有必要执行 finalize() 方法 , 如果对象没有覆盖 finalize() 方法 , 或者 finalize() 方法已经被虚拟机调用过 , 那么视为“没有必要执行” 。那么对象基本上就真的被回收了 。
如果对象被判定为有必要执行 finalize() 方法 , 那么对象会被放入一个 F-Queue 队列中 , 虚拟机会以较低的优先级执行这些 finalize()方法 , 但不会确保所有的 finalize() 方法都会执行结束 。如果 finalize() 方法出现耗时操作 , 虚拟机就直接停止指向该方法 , 将对象清除 。
对象重生或死亡
如果在执行 finalize() 方法时 , 将 this 赋给了某一个引用 , 那么该对象就重生了 。如果没有 , 那么就会被垃圾收集器清除 。
任何一个对象的 finalize() 方法只会被系统自动调用一次 , 如果对象面临下一次回收 , 它的 finalize() 方法不会被再次执行 , 想继续在 finalize() 中自救就失效了 。
回收方法区内存
方法区中存放生命周期较长的类信息、常量、静态变量 , 每次垃圾收集只有少量的垃圾被清除 。方法区中主要清除两种垃圾:

  • 废弃常量
  • 无用的类
判定废弃常量
只要常量池中的常量不被任何变量或对象引用 , 那么这些常量就会被清除掉 。比如 , 一个字符串 "bingo" 进入了常量池 , 但是当前系统没有任何一个 String 对象引用常量池中的 "bingo" 常量 , 也没有其它地方引用这个字面量 , 必要的话 , "bingo"常量会被清理出常量池 。
判定无用的类
判定一个类是否是“无用的类” , 条件较为苛刻 。
  • 该类的所有对象都已经被清除
  • 加载该类的 ClassLoader 已经被回收
  • 该类的 java.lang.Class 对象没有在任何地方被引用 , 无法在任何地方通过反射访问该类的方法 。
一个类被虚拟机加载进方法区 , 那么在堆中就会有一个代表该类的对象:java.lang.Class 。这个对象在类被加载进方法区时创建 , 在方法区该类被删除时清除 。
JVM垃圾收集策略与算法

文章插图
 
垃圾收集算法
学会了如何判定无效对象、无用类、废弃常量之后 , 剩余工作就是回收这些垃圾 。常见的垃圾收集算法有以下几个:
标记-清除算法
标记的过程是:遍历所有的 GC Roots , 然后将所有 GC Roots 可达的对象标记为存活的对象 。
清除的过程:将遍历堆中所有的对象 , 将没有标记的对象全部清除掉 。与此同时 , 清除那些被标记过的对象的标记 , 以便下次的垃圾回收 。
这种方法有两个不足:
  • 效率问题:标记和清除两个过程的效率都不高 。
  • 空间问题:标记清除之后会产生大量不连续的内存碎片 , 碎片太多可能导致以后需要分配较大对象时 , 无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作 。
复制算法(新生代)
为了解决效率问题 , “复制”收集算法出现了 。它将可用内存按容量划分为大小相等的两块 , 每次只使用其中的一块 。当这一块内存用完 , 需要进行垃圾收集时 , 就将存活者的对象复制到另一块上面 , 然后将第一块内存全部清除 。这种算法有优有劣:
  • 优点:不会有内存碎片的问题 。
  • 缺点:内存缩小为原来的一半 , 浪费空间 。
  • 为了解决空间利用率问题 , 可以将内存分为三块: Eden、From Survivor、To Survivor , 比例是 8:1:1 , 每次使用 Eden 和其中一块 Survivor 。回收时 , 将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上 , 最后清理掉 Eden 和刚才使用的 Survivor 空间 。这样只有 10% 的内存被浪费 。
但是我们无法保证每次回收都只有不多于 10% 的对象存活 , 当 Survivor 空间不够 , 需要依赖其他内存(指老年代)进行分配担保 。
分配担保
为对象分配内存空间时 , 如果 Eden+Survivor 中空闲区域无法装下该对象 , 会触发 MinorGC 进行垃圾收集 。但如果 Minor GC 过后依然有超过 10% 的对象存活 , 这样存活的对象直接通过分配担保机制进入老年代 , 然后再将新对象存入 Eden 区 。


推荐阅读