为什么Java和Python的垃圾回收方法不同?
Python使用引用计数的方法来管理对象的生命周期。也就是说,如果一个对象不再被使用,它会立即被销毁。
而在Java中,垃圾回收器(GC)会在特定的时间销毁那些不再被使用的对象。
那么,Java为什么选择这种策略呢?这样做有什么好处呢?
这种方法比Python的方式更好吗?
9 个回答
Darren Thomas的回答很好。不过,Java和Python在处理对象的方式上有一个很大的区别,那就是在常见情况下(没有循环引用),Python会立即清理对象,而不是等到某个不确定的时间。
举个例子,我可以在CPython中写一些不太规范、不便移植的代码,比如
def parse_some_attrs(fname):
return open(fname).read().split("~~~")[2:4]
这样一来,我打开的文件的文件描述符会立刻被清理掉,因为一旦对这个打开文件的引用消失,文件就会被垃圾回收,文件描述符也会被释放。当然,如果我使用的是Jython、IronPython或者可能的PyPy,那么垃圾回收可能要等到很久之后才会执行;这可能会导致我先用完文件描述符,结果我的程序崩溃。
所以你应该写成像下面这样的代码
def parse_some_attrs(fname):
with open(fname) as f:
return f.read().split("~~~")[2:4]
不过,有时候人们喜欢依赖引用计数来随时释放资源,因为这样可以让代码看起来稍微简短一点。
我认为,最好的垃圾回收器是性能最好的那个,目前看来,Java风格的代际垃圾回收器表现最好,它可以在单独的线程中运行,并且有很多复杂的优化等等。你写代码的方式应该几乎没有区别,理想情况下是完全没有区别。
其实,引用计数和Sun JVM使用的策略都是不同类型的垃圾回收算法。
追踪死对象主要有两种方法:追踪和引用计数。追踪方法是从“根”开始,比如栈中的引用,然后追踪所有可以到达的(活着的)对象。那些无法到达的对象就被认为是死的。而引用计数则是每次修改引用时,相关对象的计数都会更新。任何引用计数变为零的对象就被认为是死的。
几乎所有的垃圾回收实现都有一些权衡,但追踪通常适合高吞吐量(也就是快)的操作,但暂停时间较长(也就是程序可能会有较长的卡顿)。引用计数可以在较小的块中运行,但整体速度会慢一些。这可能意味着卡顿会少一些,但整体性能会较差。
此外,引用计数的垃圾回收需要一个循环检测器来清理那些在循环中的对象,因为仅靠引用计数无法捕捉到这些对象。Perl 5的垃圾回收实现中没有循环检测器,因此可能会导致循环引用的内存泄漏。
研究人员也在努力寻找两者的最佳结合(低暂停时间和高吞吐量): http://cs.anu.edu.au/~Steve.Blackburn/pubs/papers/urc-oopsla-2003.pdf
使用引用计数有一些缺点。其中一个最常提到的问题是循环引用:假设有A引用B,B引用C,而C又引用B。如果A取消了对B的引用,那么B和C的引用计数仍然是1,这样它们就不会被传统的引用计数机制删除。CPython(引用计数并不是Python本身的一部分,而是其C语言实现的一部分)通过一个单独的垃圾回收程序来处理循环引用,这个程序会定期运行...
另一个缺点是:引用计数可能会让执行变慢。每次一个对象被引用或取消引用时,解释器或虚拟机都必须检查这个计数是否降到0(如果降到0,就要释放这个对象)。而垃圾回收就不需要这样做。
另外,垃圾回收可以在一个单独的线程中进行(虽然这可能有点复杂)。在内存很大的机器上,或者在那些使用内存速度很慢的进程中,你可能根本不想进行垃圾回收!在这种情况下,引用计数在性能上可能会有些劣势...