概述
GC需要完成3件事情:
- 那些内粗需要回收
- 什么时候回收
- 如何回收
那么该如何判断对象已死,可以被回收呢?
引用计数法
引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时计数器就-1;任何时候计数器为0时,对象就不再使用,可以被回收。
虽然引用计数法实现简单,效率也很高,但是它很难解决对象之间的相互循环引用问题。
举个例子:
分别new 2个实例对象A以及实例对象B,当A持有B的引用,同时B持有A的引用时。就算A、B不再被使用,如果使用用引用计数法标记对象,A、B都不可能被回收了。
可达性分析算法
可达性分析算法: 通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明这个对象是不可用的。
Java中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- Native方法中JNI引用的对象
对象的自救
在可达性分析算法中不可达的对象,也并不是“非死不可”,真正宣告一个对象死亡,至少要经历2次标记过程:可达性分析过程中发现没有与GC Roots 相连接的引用链,那它会被标记并且进行一次筛选,筛选的条件是否有必须要执行对象的finalize()方法。当对象没有覆写finalize()或者已经执行过finalize()(也就是finalize()最多执行一次)则虚拟机视为“没有必要执行“。
finalize()是对象自救的最后一次机会,只要重新与GC Roots关联上即可。
对象的引用
JDK1.2之后将引用分为强引用、软引用、弱引用和虚引用4种,这4种引用强度依次逐渐减弱。
- 强引用:类似“Object obj = new Object()”这类的引用,只要强引用在,垃圾收集器就永不会收集被引用的对象。
- 软引用:系统提供了SoftReference来实现软引用。软引用用来描述一下有用,但并非必须的对象。对于被软引用关联着的对象,系统在内存不足时将会把这些对象列入回收范围之内进行第二次回收。
- 弱引用:系统提供了WeakReference来实现弱引用。弱引用用来描述非必须的对象。弱引用的对象只能生存到下一次GC之前,不管系统内存是否足够,弱引用的对象都会被回收。
- 虚引用:系统提供了PhantomReference来实现虚引用。无法通过虚引用来获取一个对象,虚引用的唯一目的是在对象被垃圾收集器回收时收到一个系统通知。
方法区回收
方法区又称永久代,主要回收2部分内容:废弃的常量和无用的类。
废弃的类回收和堆中对象的回收类似,只要系统没有其他的地方引用都当前字面量,这个常量就可以被回收。
无用的类判断就比较复杂了:
- 类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载类的ClassLoader已经被回收
- 类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类。
满足以上3个条件这个类可以被回收,但是并不一定回收。
内存回收算法
JVM中常用几种内存回收算法:
- 标记-清除算法
- 标记-整理算法
- 分代收集算法
标记-清除算法
标记—清除算法分为“标记”和“清除”两个阶段:首先标记出所需要回收的对象,在标记完成后统一回收所有被标记的对象。
标记-清除算法主要有2个问题:
- 效率问题:标记和清除两个过程效率都不高
- 空间问题:标记清除后产生大量的不连续的内存碎片,空间碎片太多可能会导致分配较大对象时,无法找到足够的联系内存而不得不提前触发另一次GC。
(图片源自网络)
复制算法
复制算法:将可用内存空间分为大小相等的2块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块上面,把已使用的内存空间清理掉。
复制算法算法的优点是效率高,不用考虑内存碎片问题,但是代价是内存缩小为了原来的一半。
(图片源自网络)
新生代一般用此种算法来收集,具体算法以及其中的内存分配担保机制都砸在本文稍后解释。
标记-整理算法
标记-整理算法,标记过程与标记-清除算法一致,只是手续步骤不是直接对可回收对象进行整理,而是让所有的存活对象往一端移动,然后直接清理掉边界以外的内存。
(图片源自网络)
分代收集算法
这种算法并没有新的思想,只是根据对象存活的周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代,这样就根据各个年代的特点采用最适当的收集算法。
- 新生代:每次垃圾收集都有大批对象死去,只有少量存活,那就采用复制算法。
- 老年代:对象存活效率高,没有额外的空间进行分配担保,就是用“标记-清理”或者“标记-整理”算法。
新生代复制算法
新生代所用的复制算法是升级版的复制算法。根据IBM的研究表明,新生代中的对象98%都是“朝生夕死”的,所以并不需要按1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,比利是8:1:1.每次使用Eden和其中一块Survivor空间。当发生新生代GC时,将Eden和Survivor中的对象还存活的对象一次性复制到空着的Survivor区中,最后清理掉Eden区和使用中的Survivor区。虽然同事复制算法,但是升级版的复制算法,每次只浪费了10%的内存空间。
我们无法保证每次回收都只有不多余10%的对象存活,当Survivor空间不足时就需要依赖其他内存(老年代)进行分配担保。
内存担保就是当剩余的Survivor空间无法存放上一次GC存活下的对象时,这些对象将直接通过分配担保机制进入老年代。
(图片源自网络)
上面图片可以看到,对象在Eden和Survivor中来回复制,同时当Survivor空间不足或者对象到一定年龄后将被移动到老年代。
空间分配担保机制
在新生代发生Minor GC前,虚拟机会检测老年代最大可用的连续内存空间是否大于新生代所有的对象的总空间,如果条件成立则Minor GC是安全的。否则判断老年代连续可用内存是否大于历次晋升到老年代对象的平均大小,如果大于则尝试一次Minor GC,小于或者没有设置内存担保机制进行一次Full GC。
所谓的担保就是,当新生代进行Minor GC后仍有大量对象存活的情况下,就需要老年代进行分配担保。所谓的担保就是所有Survivor无法容纳的对象都放入老年代,但是内存回收完成之前无法知道存活的对象数量,就只能按历次晋升到老年的对象平均值作为经验值,从而来决定时候进行Full GC以让老年代腾出更过的空间。担保失败怎还是进Full GC,然后会浪费时间,但是大部分情况下担保都是有效的。
内存分配策略
对象有限在Eden中分配
大多情况下,对象在新生代Eden区中分配。当Eden区中没有足够的空间进行分配的时候,虚拟机将发生一次Minor GC。
大对象直接进入老年代
所谓的大对象指的是需要大量连续空间的Java对象。当对象大于JVM设定的阀值之后直接进入老年代,这样做的目的是避免在Eden区以及两个Survivor区中来回复制对象。
长期存活的对象进入老年代
JVM为每个对象定义了一个对象年龄(Age)计数器。每次Minor GC结束后对象存活则Age +1,当对象的年龄增加到一定程度后(默认15),将会被晋升到老年代。
动态对象年龄判定
当Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,这时年龄大于或等于该年龄的对象就可以直接进入老年代,并不需要年龄增长到阀值。
参考书籍
本文摘录、整理自周志明的《深入理解Java虚拟机》一书,如想获得更详细介绍可自己查阅此书。