对象是否存活
Java的GC基于可达性分析算法(Python用引用计数法),通过可达性分析来判定对象是否存活。这个算法的基本思想是通过一系列"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(图论称之为不可达),则证明此对象是不可用的。
无论引用计数法,还是可达性分析都离不开“引用”的概念。Java将引用分为四种(强引用、软引用,弱引用,虚引用),这四种引用强度依次逐渐减弱。
-
strong reference强引用,垃圾收集器永远不会回收存在强引用的对象。(如Object obj = new object()就是一个强引用)
-
soft reference软引用,描述一些还有用但是并非必须的对象。系统将要发生内存溢出异常之前,将会把这些软引用对象列入回收范围中进行二次回收。如果这次回收之后还是不够内存,才会抛出内存溢出异常。
-
weak reference弱引用,也是用来描述非必须对象的,但是它的强度更弱。被弱引用的对象只能活到下一次GC发生之前。当GC发生,无论当前内存是否充足都会回收弱引用对象。
-
phantom reference虚引用(幽灵引用),是最弱的一种引用关系。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
在可达性算法中不可达的对象,不会直接死亡,如果它还没有执行finalize方法,虚拟机将在一个优先级很低的线程中触发它的finalize方法(不保证完成),如果此时对象又将自己关联到引用链上,那么GC将把它移出“即将回收”的集合。
由于finalize方法只会被执行一次,所以这个“自救”过程也只会经历一次。
垃圾回收方法
1、标记清除法
首先标记所有需要回收的对象,然后统一回收所有要回收的对象。方法简单,但是效率不高,并且产生了很多内存碎片。
2、复制算法
将内存分为大小相等的两块,每次只使用一块。当一块的内存用完了,就将还存活的对象复制到另一块内存上,然后把使用过的那一半内存空间一次性清理掉。这样再分配内存时只需要移动堆顶指针,实现简单,运行高效。但是代价是实际可用内存缩小了一半,实在太高。
因为新生代大部分对象都是”朝生夕死“,所以虚拟机将内存分为1个Eden和2个Survivor空间,大小比例默认为8:1:1。当回收时,将Eden和Survivor中还存活着的对象复制到另一个Survivor空间上,然后清理掉Eden和刚才用过的Survivor空间。这样子就只需要牺牲10%的新生代内存。
分配担保:因为存在1个Survivor空间存放不了Eden和另一个Survivor空间的对象的情况,此时那些存活的对象由于无法放入该Survivor空间,将直接进入老年代。)
3、标记-整理算法
老年代对象存活率高,复制算法在这时候效率很低,而且老年代没有额外空间做分配担保,所以不适用。根据这种特点,有人提出了标记-整理算法。
首先标记所有要回收的对象,然后所有存活的对象都向一端移动,然后直接清理边界以外的内存。
4、分代收集算法
前面三个垃圾回收算法都算是铺垫,现在商业虚拟机的垃圾收集都采用”分代收集“。将Java堆分成新生代和老年代,新生代中每次GC有大量对象死去,只有少量存活,所以采用复制算法;老年代中对象存活率高,没有额外空间进行分配担保,就采用标记-清除或者标记-整理算法。
垃圾收集器
新生代:serial收集器
单线程,必须暂停其他所有工作线程(Stop The World),直到它收集结束。STW难以接受,但是简单高效。
新生代:parNew收集器
serial的多线程版本。server模式下首选,因为它可以和cms配合使用。
新生代:parallel scavenge收集器
专注于达到可控制吞吐量的收集器,主要适合后台运算而不用太多交互的任务。
老年代:serial old收集器
serial的老年代版本,单线程。使用标记-整理算法。
老年代:parallel old收集器
parallel scavenge的老年代版本。使用标记-整理算法。
老年代:cms收集器
经历四个阶段:
-
初始标记:需要STW,仅仅标记一下GC Roots能直接关联的对象,所以很快。
-
并发标记:进行GC Roots Tracing
-
重新标记:需要STW,修正并发标记期间因程序运行导致标记产生变动的那一部分对象的标记记录,时间比初始标记长,但是远比并发标记短
-
并发清除:清除对象
由于耗时最长的并发标记和并发清除过程,收集器线程都可以和用户线程一起工作,所以总体来说cms收集器的内存回收过程是和用户线程一起并发执行的,停顿很短。
缺点:cpu资源敏感,特别是cpu核数较少时;无法处理浮动垃圾;cms基于标记-清除算法,容易产生大量内存碎片。
G1收集器
最新的收集器,性能优秀,但是太复杂不再赘述。
内存分配和回收策略
-
对象优先在Eden分配,如果启动了TLAB,将按照线程优先在TLAB上分配。
-
当Eden不够空间时,虚拟机发起一次Minor GC(新生代GC)
-
大对象直接进入老年代
-
长期存活的对象将进入老年代(存活一次GC age+1,默认age 15时进入老年代)
-
Survivor空间中相同年龄所有年龄大小 的总和大于Survivor空间的一半 时,年龄大于等于该年龄的对象可以直接进入老年代
-
Minor GC时,虚拟机会检查老年代的剩余空间是否大于新生代对象总大小or历次晋升的平均大小,如果是那么就会进行Minor GC,否则认为老年代无法提供分配担保,进行Full GC。