如何确定某个对象是垃圾
在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。
这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。看下面这段代码:
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
class MyObject{
public Object object = null;
}
最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。
为了解决这个问题,在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
GC Root
常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。
一个对象可以属于多个root,GC root有几下种:
- Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
- Thread - 活着的线程
- Stack Local - Java方法的local变量或参数
- JNI Local - JNI方法的local变量或参数
- JNI Global - 全局JNI引用
- Monitor Used - 用于同步的监控对象
- Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于”JVM持有”的了。
典型的垃圾收集算法
在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。
Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。
注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
总结
在新生代中, 每次垃圾收集时都发现有大批对象死去, 只有少量存活, 那就选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集(有 eden 和survivor 供复制, 有老年代最分配担保)。 而老年代中因为对象存活率高、 没有额外空间对它进行分配担保, 就必须使用“标记-清理” 或者“标记-整理” 算法来进行回收。
发生 Minor GC, 采用复制算法, 发现
1、 复制对象无法全部放入 Survivor, 只好通过分配担保机制提前转移到老年代中
2、 大对象(长字符串或长数组等需要大量连续空间的对象) 直接进入老年代(防止大 对象在 eden 和 Survivor 中经常复制)通过-XX:PretenureSizeThreshold 参数设置 (如 3MB), 大于这个参数的直接进入老年代
3、 长期存活对象进入老年代(默认 15 岁)
Minor GC: 新对象先放入 eden 区, 当 eden 满了会触发 Minor GC。
Full GC(等于 Major GC):
1、 每次进行 Minor GC 时, JVM 会计算 Survivor 区移至老年区的对象的平均大小, 如 果这个值大于老年区的剩余值大小则进行一次 Full GC
2、 老年代空间不足时触发 Full GC, 只有在新生代对象转入或创建为大对象、 大数组 时才会出现不足的现象(大对象直接进入老年代), 分配担保
3、 永久代满(永久代 JDK8 被移除) 优化 Full GC 本身不会先进行 Minor GC, 我们可以配置, 让 Full GC 之前先进行一次 Minor GC, 因为老年代很多对象都会引用到新生代的对象, 先进行一次 Minor GC 可以提高 老年代 GC 的速度。
在 jvm 分带垃圾回收机制中, 将应用程序可用的堆空间分为年轻代和老年代, 又将年轻代分为 eden 区、 from 区、 to 区, 新建对象总是在 eden 区中被创建, 当 eden 区空间已满,就触发一次 Minor gc, 将还被使用的对象复制到 from 区, 这样整个 eden 区都是未被使用的空间, 可供继续创建对象, 当 eden 区再次用完, 再触发一次 Minor gc, 将 eden 区和 from区还在被使用的对象复制到 to 区, 下一次 Minor gc 则是将 eden 区和 to 区还被使用的对象复制到 from 区。 因此, 经过多次 Minor gc, 某些对象会在 from 区和 to 区多次复制, 如果超过某个阈值对象还未被释放, 则将对象复制到老年代。 如果老年代空间也已用完, 那么就会触发 full gc, 即所谓的全量回收。永久代的垃圾回收主要有两部分: 废弃常量和无用的类。 如没有任何 String 对象引用“abc”。 在大量使用反射、动态代理、 CGlib 等 ByteCode 框架, 动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能(回收永久代), 以保证永久代不会溢出