JVM 通过一种可达性分析算法进行垃圾对象的识别,具体过程是:从线程栈帧中的局部变量,或者是方法区的静态变量出发,将这些变量引用的对象进行标记,然后看这些被标记的对象是否引用了其他对象,继续进行标记,所有被标记过的对象都是被使用的对象,而那些没有被标记的对象就是可回收的垃圾对象了

通过上述原理可以知道,JVM 首先会通过内存池( **类似 )**对所有分配的内存进行管理,然后经过分析标记之后,对没有标记的对象进行回收。

回收主要有三种方法。

**第一种方式是清理:**将垃圾对象占据的内存清理掉,其实 JVM 并不会真的将这些垃圾内存进行清理,而是将这些垃圾对象占用的内存空间标记为空闲,记录在一个空闲列表里,当应用程序需要创建新对象的时候,就从空闲列表中找一段空闲内存分配给这个新对象。

但这样做有一个很明显的缺陷,由于垃圾对象是散落在内存空间各处的,所以标记出来的空闲空间也是不连续的,当应用程序创建一个数组需要申请一段连续的大内存空间时,即使堆空间中有足够的空闲空间,也无法为应用程序分配内存。( 也就是说不会自动 merge )

同时执行的效率也不稳定,如果存在大量的对象需要回收,花费的时间也随之而增长,导致标记和清除两个过程的执行效率都随对象数量增长而降低。

**第二种方式是压缩:**从堆空间的头部开始,将存活的对象拷贝放在一段连续的内存空间中,那么其余的空间就是连续的空闲空间。

如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行[1],这就更加让使用者不得不小心翼翼地权衡其弊端了,像这样的停顿被最初的虚拟机设计者形象地描述为“Stop The World”

**第三种方法是复制:**将堆空间分成两部分,只在其中一部分创建对象,当这个部分空间用完的时候,将标记过的可用对象复制到另一个空间中。JVM 将这两个空间分别命名为 from区域和 to 区域。当对象从 from 区域复制到 to 区域后,两个区域交换名称引用,继续在from 区域创建对象,直到 from 区域满。

每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

缺点是,如果内存中多数对象是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象。同时这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。

可达性分析法的更多细节

为什么不使用引用计数法

虽然引用计数法足够简单,但是这个算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

GC Roots

上文说到,从图论的角度,如果一个对象对于 GC Roots 来说是不可达的,那么它就是可回收的。那么 GC Roots 包含哪些呢。

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种: