从0到1理解JVM垃圾回收机制

从C++转到JAVA,首先要知道的一条是,JAVA拥有相对比较完善的内存垃圾回收机制(回想以前在中大的作业平台上写C++代码,在new之后总会忘记free,导致我每次都不能开开心心地AC,巨汗-_-!)

JAVA是一门半编译半解释的语言,在编译阶段无法确定回收垃圾的具体位置(虽然有部分是可以确定的),所以回收垃圾的任务主要交由JVM来处理,我们经常听到高手们谈到的所谓GC指的就是垃圾回收。

垃圾回收主要有两步:

  • 识别哪些是垃圾
  • 使用回收的算法将垃圾‘扫入’垃圾桶

谁是垃圾

在之前的博文里,我们知道JAVA运行时主要分程序计数器、虚拟机栈、本地方法栈、堆、方法区。前3者随着线程同生共死,当线程结束时,它们的内存就被回收了,只有剩下的堆和方法区需要我们竭尽全力地进行垃圾回收

那么,在堆和方法区里,我们怎么知道谁是垃圾呢?

寻找垃圾算法之引用计数法

熟悉操作系统的童鞋都知道,引用计数算法的主要思想是:当有个地方引用了某个对象时,该对象的计数器增加1,当那个地方不再引用该对象时,该对象的计数器减1;直到计数器的值为0,系统就认为该对象不再有用,把它标记为垃圾

一个论文引用的例子可以解释该算法:假设你是个德高望重的学者,你写了篇影响巨大的论文,那么你的论文理所当然会被许多人引用和参考,因为你的论文特别有价值;相反,如果你是个三流的研究人员,你写的东西无人问津,就会被视为“垃圾”。

这个算法最大的缺点是:它无法处理相互循环引用的情况。什么意思呢?

还是论文引用的例子:假设现在有3个三流的研究人员A、B和C,他们分别写出了3篇质量极其低下的论文,按理来说,他们的论文是“垃圾”,不会被人引用,但数据显示,3篇论文的被引用数都为1。相关部门调查后发现,A引用了B的论文,B引用了C的论文,而C又引用了A的论文,所以虽然3篇全都是“垃圾”,但却存在着彼此的引用,形成了一个环,使得人们认为3篇论文不是“垃圾”,逃过了被“回收”的危险。

因为这个缺点,引用计数法基本只作为反例,只存在于教科书上。

寻找垃圾算法之可达性分析算法

可达性分析的原理是:一个对象引用了另一个对象时,两个对象之间就存在连线,理清了所有的对象的引用关系之后,它们基本都会处在同一颗树上,此时,将不在树上的对象视为垃圾。也就是说,那些无人知晓的对象都是垃圾

这个算法的关键点是:从谁开始找呢?

(如果从垃圾开始找,那么那些不是垃圾的反倒视为垃圾,这是极为严重的噩梦!)

有种对象,它的价值比较大,不会被轻易抛弃,相对稳定,存在的时间较长,被称为GC Roots,是可达性分析算法的起始点。GC Roots一般只存在于栈和方法区中(具体是虚拟机栈中局部变量表内引用的对象、本地方法栈中Native方法引用的对象和方法区中静态变量和常量引用的对象),堆中不会有。

清除垃圾的强有力手段

知道谁是垃圾,下一步是启用算法将其清除。也许有人会笑,清除内存中的垃圾有什么难的,直接将它标记为废弃不就得了嘛,还在那边唧唧歪歪的整啥呀?

标记清除算法

这个算法和上面说的“直接标记为废弃”一模一样,先扫描一遍,利用可达性分析算法,将垃圾标记出来,再扫描一遍,将垃圾清除

这个算法太简单了,简单到不是计算机专业的人也能秒懂,但它的简单也导致了极为复杂的后果:在一整段内存上,将其中的几个地方标记为废弃,会使得这条内存看起来‘坑坑洼洼’的,也就是内存碎片。碎片一多,分配内存时就不能顺利地拿到一块大内存(毕竟内存条上只有细小的碎片)。

为了解决这个碎片的问题,人们又想到了一个方法。

标记复制算法

这个算法的标记阶段和标记清除算法一样,差别在于,它不再清除无用的内存,而是将有用的内存复制到另一片空间,剩下的这片空间称为未拆封全新区域。

举个例子,对于一块1G的内存,我们将它划分为2块512M的小内存,每次我们只使用其中一块;在垃圾回收时先找出垃圾,然后将不是垃圾的对象移动到另一块内存上,并将这一块内存清空;下一次进行回收时也是同样的原理。

但这个算法也有一个缺点:任何时候都有一块内存不会被用到,也就是说,内存被浪费了

幸好,这个缺点可以减弱到能被接受的程度。(既然每次都会浪费掉一块内存,那么,我们每次都只浪费一块非常小的内存不就行了吗?)

标记整理算法

标记出垃圾后,既不直接将其清除,也不将有效对象复制到另一块内存上,而是将有效对象复制到内存的开头,如此一来,既不会导致过多碎片,也不会浪费另一块内存。

分代收集算法

上面三个算法都仅考虑对现有垃圾的回收,未曾考虑到时间这个不可避免的因素在垃圾回收中的影响,所以现代的虚拟机都使用一种被称为“分代收集”的算法来进行实际的垃圾回收。分代收集的原理是:根据对象的已存活时长,将其划分到不同区域,在不同区域里采取不同的回收算法

一般来说,虚拟机会将堆分为2个年代:

  • 新生代
  • 老年代

在新生代里使用了标记复制算法,所以新生代又分为3个区域:

  • 一个eden亚当区(占80%空间):这个区里存放着大量鲜活的年轻对象,它们中只有少数会存活下来。
  • 两个survivor幸存者区(各占10%空间):这个区里存放的对象已经有了一定年纪,它们的部分人可能有机会迁往老年代进行养老。