概述
上文我们详细的介绍了JVM的内存区域各个区域的作用,如果对此不了解的小伙伴可以 点此学习。
说到JVM的垃圾收集,需要知道GC需要完成的3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收
JVM垃圾回收的区域,是针对线程共享的数据区域,即堆和方法区。而程序计数器、栈(包括Java虚拟机栈和本地方法栈)是线程私有的区域,随线程创建而生,随线程死亡而灭。线程私有的就不要考虑内存回收的问题了,因为方法结束或者线程结束,其内存就跟着回收了。
对象已死吗?
垃圾收集器在进行回收之前,首先要先确定哪些对象“存活”,哪些已经“死去”,然后再回收已经“死去”的对象占用的内存空间。
判断对象是否可以被回收有两种经典算法:
- 引用计数法
- 可达性分析算法
引用计数法
定义:为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
缺点:无法处理循环引用
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法
代码示例:
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
}
}
在上述代码中,a 与 b 对象实例互相持有了对方的引用,即使令 a = null,b=null 后,两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
可达性分析算法
思想:通过一系列称为“GC Roots”的对象作为根节点,通过根节点向下搜索,搜索走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连,称之为不可达对象。Object4就是不可达对象。被回收
JVM中,可以作为 GC Roots 对象如下:
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 native方法 引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 中有四种强度不同的引用类型。引用由强到若分为:强引用、软引用、弱引用、虚引用。
强引用:使用 new 一个新对象的方式来创建强引用。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
Object obj = new Object();
软引用:一些还有用但并非必须的对象。软引用关联着的对象,在系统要发生内存溢出之前,会把这些对象进行垃圾回收。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
弱引用:也是描述一些非必须对象,强度比软引用更弱,只要发生垃圾回收,它就一定会被回收。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
虚引用:又称幽灵引用、幻影引用,是最弱的一种引用。为对象设置虚引用的唯一目的是能在这个对象被回收时会收到一个系统通知。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
垃圾收集算法
JVM的垃圾收集主要收集的是Java堆上的内存,而堆又分为年轻代、老年代,根据分代的特点应用相应的垃圾收集算法可以使垃圾收集的效率最高。垃圾收集算法有3种:
- 标记 - 清除算法:适用老年代
- 标记 - 整理算法:适用老年代
- 复制算法:适用年轻代
标记 - 清除算法
标记 - 清除算法将垃圾收集分为 “标记” 和 “清除” 两个阶段。
首先标记出所有要被回收的对象,在标记完成后统一回收所有被标记的对象。
不足:
①.效率问题:标记和清除两个过程的效率都不高
②.空间问题:标记清除之后会产生大量不连续的内存碎片,内存碎片太多会导致以后需要分配较大对象时,无法找到足够的连续内存,而再次触发垃圾收集动作。
标记 - 整理算法
它在标记-清除算法的基础上做了一些优化。将所有的存活对象压缩到内存的一端。之后,清理边界以外的内存。
优点:不会产生内存碎片
复制算法
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。这样使得每次都是对半个内存区回收,不用考虑内存碎片问题。
缺点:只使用了一半的内存,即内存利用率为50%。
JVM新生代采用这种算法,但并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。
分代收集算法
根据对象存活周期不同,将Java堆空间分为年轻代和老年代。根据不同年代的特点采用最适合的垃圾收集算法。
年轻代:年轻代中每次垃圾收集动作,都有大量的对象死去,只有少量的对象存活。而标记清理 - 算法是标记要死去的对象然后进行回收,如果年轻代中采用这种算法,标记和清理阶段效率都非常低,所以年轻代不适合采用标记 - 清理和标记 - 整理算法。而复制算法,只需要付出少量存活对象的复制成本就可以完成收集。所以年轻代采用复制算法。
老年代:老年代中对象存活率高,并且没有额外的空间对它进行分配担保,所以老年代采用标记 - 清理和标记 - 整理算法。
Stop The World
寓意:表示一种全局停顿现象。JVM 在进行垃圾收集时,可能会暂停所有用户线程。JVM 采用不同的垃圾收集器 (下面介绍),用户线程在JVM进行垃圾收集时暂停的时间是不同的。
为什么 GC 时会产生全局停顿现象?
举个栗子:同学们聚会时打扫房间,聚会时很乱,有的同学嗑瓜子,有的同学玩游戏等。就是不断有新的垃圾产生,房间永远都打扫不干净。只有让大家都停止活动了(暂停所有用户线程),才能将房间打扫干净。