本文参考《深入理解Java虚拟机》,主要介绍GC相关的算法,引用计数法、可达性分析算法、垃圾收集算法(分代收集理论,标记-清除/整理/复制)
本系列其他文章链接:
JVM(Java Virtual Machine)垃圾收集算法篇
当对象不在被使用的时候,就被当做垃圾给GC掉以节省内存空间,而大部分GC都发生在堆内存中,因为这个区域是所有Java线程共享的,最容易OOM的地方
引用计数算法:在对象中添加一个引用计数器,每当有一个地方引用它的时候,计数器值加一;当一个引用失效的时候,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的。
在特定情况下,这个效率还是比较高的算法,但是还是会有限问题存在:相互引用导致无法被回收即使他们不在被使用了,如下图示:
图中,对象56,当他们互相引用,但是却没有人使用他们之中任意一个,使用计数器计数,他们的值永远不为0,就不会被回收,导致内存泄漏的问题。所以,在单独使用计数器是没有办法解决垃圾回收标记问题的。
这个算法的基本思路就是通过一系列称为:“GC Roots”的根对象作为起始点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则说明这个对象不可达,证明此对象不可能再被使用的。如下图所示:
在Java技术体系,固定可作为GC Roots的对象包括以下几种:
无论是引用计数器还是可达性分析,判断对象的存活都和“引用”离不开关系。
在JDK1.2之前,Java里面的引用是很传统的定义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为“强/软/弱/虚引用”,这四种引用强度依次减弱
大多数垃圾收集器都基于“分代收集理论”进行设计,这套理论实际上是一套符合大多数程序运行实际情况的经验法则,它建立在两个假说之上
依据这个理论,收集器将Java堆划分不同的区域。一个区域放置朝生夕死的对象,一个区域放置熬过多次垃圾收集还“活着”的对象。
对于不同的区域(新生代、老年代),则根据不同的区域使用不同的垃圾收集算法,因此有了“Minor GC”、“Major GC”、“Full GC”,
标记-清除算法:首先标记出所需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,或者反过来。这个算法效率是它的优点,因为标记完,回收掉就可以了,不需要其他操作,所有这也成了最大的缺点,容易出现内存碎片,如下图:
产生大量不连续的内存碎片,可能会导致分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-复制算法的底层思想是“半区复制”,它将内存按容量划分大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将存活的对象复制到另一块上,然后把使用过的那一块清空,等待下一次交换使用。
这样的垃圾收集过程中就只需移动指针位置即可,不会存在内存碎片的问题,实现简单,运行高效,但是这样有一个比较大的问题,内存始终都是有一块在某种意义上是浪费的。
但是为我们所知道的,对象大部分都是朝生夕死的,98%的对象都熬不过第一次垃圾回收,所以并不需要1:1比例来划分新生代的内存空间。在HotSpot虚拟机中,默认分配比例大小是Eden:Survivor(from:to) == 8:1:1,也就是说每次内存分配只使用Eden和Survivor的另一块。
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记-整理算法就是让所有存活的对象向一端移动,然后清理掉边界以外的内存。
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,后者是移动式的。