目的:
使用垃圾回收器的唯一原因就是:回收程序不再使用的内存。
针对的目标对象:
Java的垃圾回收器会自动回收不再使用的Java对象,释放内存。但是回收的是用new创建的,分配在堆上的内存。
finalize():
那么,如果不是用这种方式创建的对象,该怎么回收?比如:Java调用了本地的c语言方法创建了个对象,那么这时,该对象不是放在堆上的。除非你手动去调用c的free()方法,否则,这个对象将永远不会被清理。
Java的finalize()方法可以解决上面的问题。垃圾回收器在回收垃圾对象时,会首先去调用该对象的finalize()方法。所以,你可以在finalize()方法中调用c的free()方法。
一般教科书会写,finalize()用于垃圾回收之前的清理工作,而实际上,除了上面讲的极少数情况之外,我们一般情况下并不需要使用finalize()。
不保证发生:
虽然Java的垃圾回收器会根据对象的使用情况自动清理内存,但并不一定会发生,如果内存还够用的话,虚拟机一般是不会浪费时间去作清理工作的。
如何判断Java对象可以回收:
1.不被使用的“引用计数器法”:
每个对象都含有一个引用计数器,当有引用变量指向该对象时,引用计数器+1,当这个引用变量不再指向该对象,或者被置为null时,计数器-1。如下图:
当第四种情况发生时,即:没有引用变量指向“李四”那个对象了,这时,垃圾回收器在恰当的时候就会把李四所在的对象回收掉。
它简单便捷,但是之所以没被Java虚拟机采用的原因是:无法解决循环引用的问题。举个简单的例子:
objA有个instance变量,objB也有个instance变量,让objA的instance指向B对象,而让objB的instance变量指向A对象,那么,B对象和A对象的引用计数器都是1,不为0,如果按照引用计数器的方法,A和B就不能被回收,但事实是,objA和objB这两个引用变量已经是null了(它们指向的具体对象已经不再被引用了)。
2.根搜索算法
在主流的商用程序语言中(Java和C#,甚至古老的人Lisp语言),都是使用根搜索算法(GC Roots Tracing)判定对象是否存活的。
之前讲过,对象的引用是放在栈中的,常量的引用是放在常量池之中的。如图:
根搜索算法的思想是,从常量池和栈中的引用变量开始遍历所有的引用变量,找到所有的活的对象(引用不为null)。然后再继续寻找这个对象所包含的所有引用,反复进行,直到所有引用网络被访问完为止。
常量池或栈中的引用变量是根节点,扩展出的整个网络就是一个引用链。最后,如果最终发现有对象到根节点的路径是不可达的,说明这个对象是可回收的,这就解决了循环引用的问题:
如上图,GCRoots是根节点,object5、6、7虽然各自引用,但是它们到GCRoots都是不可达的,所以,它们是可以被回收的。
怎样回收?
每个虚拟机采用的回收算法是不同的,经典的案例如下:
标记-清除算法:
在使用“根搜索算法”寻找引用变量的同时,虚拟机会给每个存活的对象做一个标记,全部标记完成的时候才进行清除工作。
这样的问题是,存活的对象在堆中不是连续存储的,那么清除“死亡”对象后,内存中就会留下大量碎片,如果在后面需要用到大内存对象时,内存空间不够,就要重新整理内存。如图回收前:
回收后:
复制算法:
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如图回收前:
回收后(把存活着的对象搬到右侧,左侧剩下的就都是可清理的,然后统统清理掉。当右侧需要清理的时候,类似的,把存活的对象再搬到左侧,然后清空右侧):
这种方式的缺点:很显然,可用内存只有原来的一半儿。还有个缺点:如果左侧大量的都是存活的对象,清理时仍然要全部搬到右侧,很浪费时间。
现在的商业虚拟机都采用这种收集算法,但是保留区与运作区的比例有不同,且详细又将堆内存划分为新生代、老年代。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。关于新生代、老年代、堆内存等,详细可查阅关于Java虚拟机的资料了解。