Tuesday, December 23, 2008

程序语言的垃圾回收机制

我们在程序中要用到很多的对变量,这些对象和变量都存储在内存中,而这些变量不全部是在整个程序的运行过程中都有效的,所以一些已经没用的变量同样在占用着内存,显然这是不可取的。所以在程序的运行过程用,应该将已经没用的变量所占用的内存释放出来存放其他的变量。

在C++中,需要明确的释放这些内存,即程序员需要使用代码来释放变量所占内存。但在JAVA中,当没有对象引用志向原先分配给某个对象的内存时,该内存便成为了垃圾。JVM的一个系统级线程会自动释放该内存块。垃圾收集意味着程序不再需要的对象是无用信息,这些信息将被丢弃。 

由于创建对象和垃圾收集器释放丢弃对象所占的内存空间,内存回出现碎片,碎片是分配给对象的内存块之间的空闲内存洞。这些碎片可能存放不了一个变量,但这些内存合并到一起却能做很重要的事情,所以在垃圾收集的同时也应该清除内存记录碎片。碎片整理将所占用的内存移到堆的一端,JVM将整理出的内存分配给新的对象。 垃圾收集能自动释放内存空间,减轻编程负担,保护了程序的完整性。其潜在的缺点是它的开销影响程序性能。

JVM必须跟踪程序中有用的对象,而且释放没用的对象。这个过程需要花费处理器的时间,还有,垃圾收集算法不完备性,早先采用的一些垃圾收集算法不能保证100%收集到所有的废弃内存。但是这些缺点却掩盖不了垃圾回收机制光芒四射的影响力,而且,随着垃圾收集算法的不断改进以及软硬件运行效率的不断提升,这些问题都将迎刃而解。 说了这么多垃圾回收的过程,那么JVM是怎么知道哪些内存是废弃的哪些是有用的呢?那就要看JVM使用了什么样的算法了。

虽然Java语言规范并没有告诉明确说明JVM使用哪种垃圾回收算法,但是任何一种垃圾收集算法一般都要两件基本的事情:发现无用信息对象;回收无用对象占用的内存空间(并进行碎片整理,所以我们程序中的有些对象的句柄可能会发生变化)。

根集是指正在执行的Java程序可以访问的应用变量的集合,包括局部变量、参数以及类变量等。大多数的垃圾回收算法都使用了根集。垃圾回收集首先需要确定从根开始哪些是可达的哪些是不可达的,从根集可达的对象都是活动对象,否则就符合了垃圾收集的条件,应该被收回。

唯一没有使用根集的垃圾回收算法就是引用记数法。它使用计数器区分存活对象和不再使用的对象。堆中的每个对象对应于一个应用记数器,当每一次创建一个对象并赋给一个变量时,引用记数器设置为1。当对象被赋给任意变量时,引用计数器每次加1,当对象出了作用域后,即对象丢弃不再使用,引用计数器减1。一旦计数器为0,对象就满足作为垃圾的条件了。基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适用于实时间运行的程序。但引用计数器的加减,增加了程序执行的开销。

Tracing算法是为了解决引用计数器算法的问题而提出的,它使用了根集的概念。基于Tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象。

Compacting算法是为了解决堆碎片问题而提出的。在清除过程中,将所有的对象移到堆的一端,另一端变成了一个相临的空闲内存区,收集器对移动的所有对象的所有引用进行更新,使得这些引用在新的位置能识别原来的对象。一般增加了句柄和句柄表。

Copying算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。将堆分成一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满时,基于Copying算法的垃圾收集从根集中扫描活动对象,并将每个活动对象复制到空闲面,这样活动对象所占的内存之间就没有空闲洞,空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。在切换过程中程序暂停执行。此算法的缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这就导致了算法的低效。

在程序设计中有这样的规律:多数对象存在的时间比较短,少数存在时间比较长,那么就可以将堆分成两个或多个,每个子堆作为对象的一代。由于多数对象存在的时间较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。这样,上次运行存活下来的对象移到下一最高带的子堆中,由于老一代的子堆不会经常被回收,因而节省时间。这种算法叫Generation算法。

No comments:

Post a Comment