在java程序中,对象用完后就弃之不理的做法是不安全的,当然在程序运行的时候GC负责回收无用对象占据的内存,
Q:那么GC是否能释放所有内存呢?
其实对于垃圾回收器只知道回收那些通过new分配的内存,对于并非使用new获得内存它不知道怎么去释放的;为了应对这种情况,java中允许定义一个finalize()方法来释放非new占有的内存,当gc准备好释放对象占用的存储空间前,将先调用对象的finalize()方法,并且在下一次垃圾回收的时候才进行真正的回收;所以在finalize()方法里面可以一些重要的清理工作。还有java程序一般在内存吃紧的时候时候才会发起GC的,在内存够用的时候也许不会发起GC,因为GC也是有开销的。
finalize()用途:
之所以要有finalize()是由于在分配内存时可能采用类似C语言中的做法,使用JNI native方法来申请内存,也就是我们通常说的“本地方法”,这些“本地方法”只支持C,C++,但是它们可以调用其他任何程序代码来获取内存;在C中会调用malloc()系列函数来分配内存,而除非调用free()函数来进行内存释放,否则内存得不到释放会造成内存泄露;从而在finalize()使用本地方法调用free()函数来进行内存释放,防止内存泄露;
finalize()并不依赖外部调用,当程序强制调用或者程序主动调用System.gc()时,对象的finalize()方法会被调用到,所以在finalize()方法里面你可以进行一些对象释放达到内存清理的目的。
Q:GC是如何工作的?
当问到这个问题的时候,程序员会首先想到:引用计数法;其实引用计数法是一种简单但是速度很慢的垃圾回收技术;每个对象都包含一个引用计数器,当有引用增加,该对象的引用计数+1;当引用离开或者被重置null时,该对象的引用计数-1;虽然管理引用计数的开销很小,但是如果使用它进行内存回收,那么它的开销将贯穿到整个程序运行生命周期中。使用引用计数法,GC在进行释放时,需要遍历对程序所有对象进行遍历,当发现引用计数为0的对象,就释放,但在这样做会存在问题,如果对象被循环引用,可能会出现引用计数一直不为0的对象,这样的对象一直得不到释放;所以引用计数通常只是用来说明GC的工作方式,但是并没实际使用到java虚拟机中。
在一些更快的模式中,垃圾回收器并非使用引用计数的方式。它们依据的思想:对于任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用;这引用链可能会穿过数个对象层次,因此,如果从堆栈和静态存储区开始,遍历所有引用,就能找到所有“活”的对象;对于发现的每个引用,必须追踪它所引用的对象及该对象包含的所有引用,如此反复循环,直到堆栈或静态存储区之中的引用所形成的引用链完成被访问都为止。所有访问到的对象都必须是“活”的,那么这样就解决循环引用的问题,因为这种“交互自引用的对象组”根本不会被发现,那么也就会被GC掉了。在这种方式下,java虚拟机将采取了一种自适应的垃圾回收技术,至于如何找到“活”着的对象,取决于不同的java虚拟机实现方式。
停止-复制(stop and copy):先将程序暂停运行,然后将所有存活的对象从当前堆复制到另外一个堆,没有被复制的全部都是垃圾可以被回收;
对于停止-复制(stop and copy)缺点:浪费内存空间,如果对象存活率较高时要执行较多的复制操作,效率降低。
程序进入稳定状态之后,可能会产生少量垃圾,甚至没有垃圾,尽管如此,复制式回收器仍然会将所有的内存从一个堆复制到另外一个堆,这样很浪费。为了避免这种情况,一些java虚拟机会进行检查:要是没有新垃圾产生,就会进入另一种工作模式:标记-清扫(mark and sweep),SUN公司早期版本java虚拟机就是使用这种模式。对于一般用途而言,标记-清扫(mark and sweep)速度相当慢,但是对于程序产生少量垃圾或者不产生垃圾时,它的速度就很快了。
标记-清扫(mark and sweep)所依据的思路同样是从堆栈和静态存储区出发,遍历所有引用,找出所有存活对象;每当找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象;只有全部标记工作完成的时,清理动作才会开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作;所以剩下的堆空间不是连续的,垃圾回收器要是希望得到连续的空间的话,就得重新整理剩下的对象。
对于标记-清扫(mark and sweep)缺点:回收了被标记的对象后,导致很多内存碎片。
在java虚拟机中,内存分配以较大的“块”为单位;如果对象较大,它会占用单独的块;严格来说,停止-复制(stop and copy)要求在释放旧对象之前,必须先把所有存活对象从旧堆复制到新堆,这将导致大量的内存复制行为。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里面拷贝对象了,每个块都用相应的代数(generation count)来记录它是否存活;通常,如果块在某处被引用,其代数会增加;垃圾回收器将代数为0的块进行整理。这对处理大量短命的零时对象很有帮助。垃圾回收器会定期进行完整的清理动作--大型对象仍然不会被复制(只是代数增加),内含小型对象的那些则被复制并整理。java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到标记-清扫(mark and sweep)模式;同样java虚拟机也会跟踪标记-清扫(mark and sweep)的效果,要是堆空间出现很大碎片,就会切换为停止-复制(stop and copy)模式;总之新式的垃圾回收器是一个自适应,分代的,停止-复制的,标记-清扫的复合的垃圾回收器。