关于java的垃圾回收机制,网上的牛文一堆。我写这个文章,只是我对这个问题的一点理解,看完这些牛文之后的一点点总结。至于实际上JVM是不是这么搞垃圾回收,这个确证起来还是比较复杂的。
首先,为什么要垃圾回收?
因为内存有限,程序产生的一些垃圾必须清理,否则再大的内存也扛不住。C++这个语言赋予程序员对内存的使用权还比较多,开辟内存清理内存,都是程序员的事情。到了java,程序员貌似不需要太考虑内存的事情了,这些都有java虚拟机接管了。java程序员只需要了解一些java到底怎么回收内存的,但是不了解这个,对编写好的java程序影响也不大吧(这个是我个人猜测,我只算个java初学者)。
第二,java怎么找到那些内存已经变成了垃圾?
第一个方法,引用计数。这个似乎是个很古老的方法,现在JVM应该不用它了。引用计数就是记录每个对象的引用它的次数,当这个引用次数变为0的时候,说明它没用了,可以被清理走了。这个问题有什么缺点呢?很明显如果A引用了B,B引用了A,这种循环引用会带来麻烦,因为A和B的引用次数永远不会成为0,但是A和B可能都没用了。
第二个方法,对象引用遍历。对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集。在对象遍历阶段,GC必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。
第三,找到垃圾了之后,怎么处理?
放着不管肯定是不行的,因为这涉及到JVM分配内存的问题。Java在堆中分配对象,在一些语言中,在堆上分配对象的代价十分高昂,但是,java不是这样,java从堆分配空间的速度可以和其他语言从堆栈上分配空间的速度相媲美。Java能做到这个就和它的内存回收机制有关系。拿think in java的比喻,java中堆的实现像一个传送带,没分配一个新对象,他就往前移动到一格。这个就必须保证内存的紧凑,不能出现太多的内存片段。所以JVM在回收垃圾的时候,必须做一些其他工作,保证这个紧凑性。
第一个方法:stop-and-copy。这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,JVM生成的新对象则放在另一半空间中。GC运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。并且对于指定大小堆来说,需要两倍大小的内存,因为任何时候都只使用其中的一半。注意,在执行这一操作的时候,要先暂停程序的运行。
第二个方法:mark-and-sweep.它依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找到所有存活对象。每当它找到一个存活对象,就会给对象设一个标记,这个过程不会回收任何对象。只有全部标记工作完成的时候,清理动作才开始。在清理的过程中,没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收期要是希望得到连续空间的话,就得重新整理剩下的对象。mark-and-sweep也要在程序暂停的情况下才能进行。
三,增量收集器。增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾,也可理解为把堆栈分成一小块一小块,每次仅对某一个块进行垃圾收集。这会造成较小的应用程序中断时间,使得用户一般不能觉察到垃圾收集器正在工作。
四,分代收集器。复制收集器的缺点是:每次收集时,所有的标记对象都要被拷贝,从而导致一些生命周期很长的对象被来回拷贝多次,消耗大量的时间。而分代收集器则可解决这个问题,分代收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。JVM生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象(非短命对象)将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。
五,并行收集器。并行收集器使用某种传统的算法并使用多线程并行的执行它们的工作。在多CPU机器上使用多线程技术可以显著的提高java应用程序的可扩展性
PS. 关于finalize()
这个方法在对象被垃圾回收时调用。因为垃圾回收这这事不太可控,因为只要内存够用,可能JVM永远不启动垃圾回收,毕竟这个一个很耗时耗力的工作,所以,尽量少用finlaize()。这个终止器的用途类似于C++里的析构函数,而且都是自动调用的。但是,两者的调用时机不一样,使两者的表现行为有重大区别。C++的析构函数总是当对象离开作用域时被调用。这就是说,C++析构函数的调用时机是确定的,且是可被应用判知的。但是,Java终止器却是在对象被销毁时。由上所知,被丢弃的对象何时被销毁,应用是无法获知的。而且,对于大多数场合,被丢弃对象在应用终止后仍未销毁。