垃圾收集GC(Garbage Collection)是Java语言的核心技术之一,是java在内存策略上区别于其他语言如C、C++的一种技术。
要聊聊java的垃圾回收机制,首先就得说说java的对象是怎么产生的。不同于C跟C++,java会在堆上分配对象,然而我们都知道在堆上分配对象的代价是十分昂贵的。打个比方,在C++里面,堆分配对象的方式就像一个院,里面每个对象都负责管理自己的地盘,一段时间后,对象可能被销毁,但是地盘必须被加以重用,这是就需要逐个空间查找,这种代价是很大的。然而java不同,java的堆就像传送带,引入了堆指针,分配新对象只是指针的简单移动,虽然在记录指针位置上会有一点花费,但相比之下效率远高于C++的堆。不仅如此,为了防止频繁的内存页面调度和使堆中的对象更加紧凑,java使用了垃圾回收机制。(机制如图)
那么java的垃圾回收机制的原理是什么呢?
首先给大家介绍一种叫引用计数的垃圾回收技术。用了这种技术的每个对象都含有一个引用计数器,当有引用接至对象时,引用计数加1。当引用离开作用域或被置为null时,引用计数减1。虽然管理引用对象的开销不大,但是这项开销在程序整个生命周期中持续发生。垃圾回收器将在含有所有对象的列表上遍历,当发现某个空间计数为0时,才释放其空间。这种方法的缺陷是,一效率低,因为需要遍历所有对象的表才能确定需要回收的空间,速度慢;二是存在本需要被回收的对象间循环引用,但是却因为其计数不为0而无法被正确回收。
在一些更快的模式中有采用“追溯引用”的方法。这种方法的思想是:对任何“活”的对象,一定能追溯到其存活在堆栈或静态存储区之中的引用。这个引用可能穿过多个对象层次。java虚拟机会从堆栈和静态存储区开始,遍历所有引用,就能找到所有对象。为什么说这里比上面引用计数的方法效率高呢?一方面因为这里访问到的只是活的对象。另一方面是因为“交互自引用的对象组”不会被访问到,也就自然跟着其他“死”对象一起被回收了。
那么回收空间后,java又是用什么方式来回收整理空间的呢?
这里有两种方式,一种叫停止—复制。这种机制是这样子的,先停止程序,然后将活的对象全部从当前堆复制到另一个堆上紧密排列,没有被复制的都是垃圾并被回收。但是这种技术有个问题,就是需要停止程序,需要占用一片完全大小的堆,需要在复制后重置引用与地址之间的关系,所以效率很低。为了解决这个问题,java做法是把一个堆里面分块,在一个堆里的块间复制移动。然而另一个问题是程序进入稳定状态后根本不会产生很多垃圾,这是这种复制回收器的回收方式就显得“小题大做”。为此就引入了第二种方法,叫“标记—清扫”法(只适用于少量垃圾,不然效率很低),思路是从堆栈和静态存储区出发,遍历所有引用,进而找出所有存活对象。每找到一个存活对象,就给其一个标记,在标记工作完成后,释放没被标记的对象内存,此时剩下的堆空间不连续。现在的java虚拟机,运用这两种方式,在回收对象少时标记回收,在回收对象多时复制回收,并且大型对象(快占用一个块的)只用标记回收,减少复制移动成本。