不同于C++,Java不需要使用者手动分配和释放内存,jvm会自动帮你回内存。但是免费的往往是最贵的,当你依赖于jvm内存回收时,很多内存回收的细节就容易被你忽略掉,当真正因为内存回收导致应用卡顿STW时,就会让你变大头,所以了解jvm内存回收的机制能帮我们更好的驾驭jvm,当真的遇到gc引起的性能问题时,我们也可以进行针对性调优。

哪些内存需要被回收?

首先,回收内存我们首先要知道哪些内存需要被回收。前面我们聊到,运行时内存主要分为5个部分。其中线程私有的内存程序计数器,虚拟机栈,本地方法栈他们随线程生随线程灭,而且大小在编译期间就可以基本确定,这部分内存我们是不需要考虑太多回收相关的事宜。但是堆和方法区这两个线程共享的区域则不一样,因为这部分内存和你的代码流程及分支相关,是动态的,虚拟机不清楚你的代码到底需要使用多少内存,因此,垃圾回收关注的主要也就是这两块内存。

怎么判断对象是否存活?

对于堆内存的回收主要是对那些已经不会再被用到的对象进行回收。那么怎么判断对象是不是已经不再被使用(死亡)或者仍然被引用呢(存活)。主流的方法有两种:

1.引用计数法。即为每个对象的引用次数做一个计数,让对象被其他对象引用时,计数器+1,当引用失效时,计数器-1。那么在某个时间点如果需要确定对象是不是存活,只需要判断计数器的值就行,如果值为0说明对象不再被其他对象引用,即对象已经死亡,那么这些对象就是我们要回收的目标对象。然而JAVA语言并没有使用这种方式,因为这种方式很难解决对象之间的循环引用问题。

2.根搜索算法。对与所有的对象组成的集合我们可以看成是一个图,对象与对象间的引用关系我们可以看做是图中的边,对象是图中的点,通过一系列GC roots 对象做起点,开始进行图的搜索,被遍历到的对象,我们认为是存活的,那些未被遍历到的对象是引用不可达的,他们就是要被回收的对象。jvm中用的正是这种算法,而能被作为GC Roots 的对象主要包括下面几种:

  • 虚拟机栈和本地方法栈中引用的对象
  • 方法区中类的静态属性引用的对象
  • 方法区中常量引用的对象

垃圾回收算法

Mark-Sweep(标记-清除)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
JVM垃圾回收 - 图1
从图中可以很容易看出标记-清除算法实现起来比较容易,但是又一个比较严重的问题,就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

Copying(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

JVM垃圾回收 - 图2
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

Mark-Compact(标记-整理)算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
JVM垃圾回收 - 图3

Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。