您好,欢迎访问三七文档
Java垃圾收集算法简介任何垃圾收集算法必须完成两件事情。首先,它必须检测出垃圾对象。其次,它必须回收垃圾对象所占用的堆空间并使之对程序重新可用。垃圾检测通常通过定义一个根引用集并计算其可达对象集的方式来实现。一个对象,如果可以通过某条始于根引用的引用路径而被执行程序访问到的话,则称其为可达的(reachable)。对程序而言,根引用始终是可以访问的。一个对象如果是可达的,则称其为活动对象;否则就被称为垃圾,因为它对程序的未来执行不再有任何影响。根引用集的构成取决于JVM的具体实现,但总是包含所有栈帧中局部变量区和操作数栈所持有的引用以及保存在静态变量中的引用。根引用的另外一个来源是常量池,已装载类的常量池可能保存有对堆中一些字符串的引用,这些字符串往往是类名、父类名、父接口名、域名、域签名、方法名和方法签名等。根引用还有一个可能来源就是那些传递给本地方法(nativemethod)但尚未被其释放的引用。根引用的另一个潜在来源是JVM的运行时数据区,因为某些实现会把JVM运行时数据区的一部分放在堆上,例如方法区中的类数据本身。可达对象集包含所有通过根引用可以直接或间接被程序访问到的对象,从技术角度讲,可达对象集是“指向”关系下根引用集的传递闭包。区分活动对象和垃圾的两个基本方法是引用计数和追踪。JDK中的标准垃圾收集器全部采用了追踪的办法,虽然具体形式各不相同。一、引用计数收集器引用计数是垃圾收集的早期策略。在这种方法中,堆中的每个对象都有一个引用计数。当对象被创建并且指向其引用被赋值给一个变量后,该对象的引用计数被设置为1。以后每当其引用被赋值给一个不同的变量时,该对象的引用计数就加1。当持有该对象引用的变量离开其作用域或者被赋给一个新值时,该对象的引用计数就减1。任一对象一旦其引用计数变为0,就成为垃圾。一个对象一旦被当作垃圾收集后,它所引用的所有其他对象的引用计数必须相应递减。这样,对一个对象的垃圾收集可能引发连续的对其他对象的垃圾收集。该方法的好处是,引用计数收集器算法简单,适于做增量收集,对于程序不能被长时间打断的实时环境特别适合,另外,收集过程也有助于改进引用局部性。坏处就是,引用计数无法检测出不可达的循环结构(两个或多个对象之间相互引用),因为它们的引用计数永远不会为0。另一个坏处就是每次增减引用计数都带来额外开销,而且该算法还需要编译器的高度配合。正是由于这些固有缺陷,引用计数算法在生产环境中很少使用。二、追踪收集器追踪收集器从根引用开始探寻并描画对象引用图。探寻过程中遇到的对象会以某种方式被打上标记。通常来说该标记既可保存在对象本身,也可保存在单独的位图中。探寻结束后未被标记的对象就是不可达的对象,可被当作垃圾收集。基本的追踪算法被称作“标记并清理”(markandsweep)。该名字指出了垃圾收集过程的两个阶段。在标记阶段,垃圾收集器遍历引用树,标记每一个遇到的对象。在清理阶段,未被标记的对象被释放,相应内存被返还待用。在JVM中,清理阶段必须包括对象的了结(finalization)。标记并清理算法实现简单,可以轻易回收循环结构,而且不存在为维护引用计数而付出的额外开销和对编译器的依赖。但是它也有不足,其中最大的问题是,在清理阶段,堆中的所有对象,不论是否可达,都会被访问。一方面这对于可能有页面交换的堆所依赖的虚存系统有着非常负面的性能影响;另一方面,因为其中很大一部分对象可能是垃圾,这就意味着垃圾收集器把大量精力都花费在检查和处理垃圾上面了。无论从哪个角度来看,该算法都可能产生收集暂停时间过长、收集开销偏大的问题。标记并清理收集器的另一个不足是它容易导致堆的碎片化,从而引发引用局部性或者大对象分配失败等方面的问题。三、压缩收集器JVM的垃圾收集器很可能拥有一个对付堆碎片的策略。标记并清理收集器通常采用的两种策略是压缩或拷贝。这两种方法都是通过快速移动对象来减少堆碎片。压缩收集器把活动对象越过空闲内存区滑动到堆的一端,在这个过程中,堆的另一端就变成了一块大的连续空闲区。所有指向被移动对象的引用也被更新,指向新的位置。为了更好的理解压缩过程,可以将堆比作书架的一格,其中一部分放满了不同厚度的图书。空闲空间就是图书之间的空隙。压缩就是将所有图书朝一个方向推移,以弥合所有空隙。它从最靠近隔板的图书开始,将它推向隔板,然后将离隔板第二近的图书推向第一本图书,接着将第三本图书推向第二本图书,依此类推。最后,所有图书在一端,所有空闲空间在另一端。被移动对象的引用更新可以通过为对象引用添加一层间址而得到简化。对象引用不再直接指向堆中的对象,而是指向对象句柄表中的一个表项,该表项中的对象句柄才直接指向堆中的实际对象。这样,当对象被移动时,只需在对象句柄表中更新其句柄,执行程序中所有指向该对象的引用都不必再更新。这种方法简化了消除堆碎片的工作,但增加了每一次对象存取的开销。四、拷贝收集器拷贝收集器同样使用追踪技术,它把所有的活动对象移动到一个新的区域,而原有区域就全部变成了空闲空间。由于被移动对象在新的区域中被紧挨着放置,因而在原有区域时对象之间可能存在的空隙也被消除。对象的拷贝可以在追踪过程中即时进行,不必通过标记和清理两个单独的阶段来完成。对象被实时拷贝到新的区域后,它在原有区域中的副本被一个转向指针(forwardingpointer)所取代,该指针指向该对象在新的区域中的副本。转向指针让垃圾收集器检测出那些引用(其所指对象已经被移动到新的区域中)并以转向指针的值更新之,从而使它们指向对象的新位置。一个通用的拷贝收集器算法被称作“停止并拷贝”(stopandcopy)。在这个方案中,堆被分成两个区域,任何时候都只使用其中的一个区域。对象在同一区域中分配,直到该区域的所有空间被耗尽。此时,程序执行被中止,堆被遍历,遍历过程中遇到的活动对象被拷贝到堆的另一个区域。当停止并拷贝过程完成后,程序恢复执行,对象的内存将从堆的这个新区域中分配,直到它也被用尽。那时程序将被再次中止,堆被遍历,活动对象被拷贝回原来的区域。该方案的代价是所需内存是指定堆空间的两倍,因为不论何时都只有一半的内存被使用。拷贝收集算法的优点是只访问活动对象,垃圾对象不会被检查,自然也无需被换页到内存或被缓存,收集过程所用时间只取决于活动对象的数量。这样既避免了不必要的收集开销,又最大限度地减少了收集暂停时间。不过,除了额外的内存消耗外,拷贝收集器还需要承担对象拷贝和引用更新所带来的成本增加。这一点在长寿对象较多时体现得更为明显,因为每次收集时它们都要被来回复制。五、分代收集器通过观察和实验,对多种语言所写的大多数应用程序而言,它们所创建的对象都具有如下特征:1)大部分对象的寿命都很短,但总有一些对象会活得足够长;2)年长对象很少引用年幼对象。以上事实也被称作“弱代假说”(weakgenerationalhypothesis),它是分代收集算法的前提和基础。在这种方法中,对象按照年龄分成组(代),堆则被划分成两个或多个子堆,每个子堆服务于一代对象,因此子堆也常常被称作某某代。最年幼代被进行最频繁的垃圾收集。因为大部分对象的寿命都很短,所以只有很小一部分的最年幼对象经历首次收集后还能存活。如果一个最年幼对象在经历几次垃圾收集后依然存活,那么它将被提升到寿命更高的一代(被移动到另一个子堆中去)。与相对年幼代比较,相对年长代被垃圾收集的频率总会有所降低。随着对象在其当前代中的不断成熟(经历多次垃圾收集而不死),最终它们会被移动到较其当前代更为年长的一代中。分代收集技术既可应用于拷贝算法,以解决它在处理长寿对象时效率低下的问题,也可应用于标记并清理算法。不管在哪种情况下,把堆划分为对象代都有助于提高最基本的垃圾收集算法的效率。六、自适应收集器自适应收集算法利用如下事实:一些收集算法在某些情况下工作的更好,而另一些则在其他情况下工作的更好。自适应收集器监控堆的当前状态并据此对其所用垃圾收集技术做相应调整。它可能只在程序运行的同时对单一收集算法的参数做出调整,也可能从一种算法快速切换到另一种算法,甚或还可能把堆划分为子堆并在不同的子堆上同时使用不同的算法。使用自适应方法,JVM实现的设计者无需再选择某种特定的垃圾收集技术。他们可以使用多种技术,为每种算法分配最适合它的工作。实际上,在现代JVM实现中,大部分垃圾收集子系统都具有某种程度的自适应能力,很多时候我们只须选择策略并设定目标便能得到满意的结果,至于具体的收集算法选择和参数配置则可交由垃圾收集子系统自行处理。Sun的HotSpotJVM中的垃圾处理子系统就是这样运作的,具体细节有时间再专门讨论。HotSpotJVM中的垃圾收集(2010-03-2505:51:49)从J2SE5.0开始,HotSpotJVM共包含四种垃圾收集器,它们全部基于分代算法。一、代的划分HotSpotJVM中内存被划分为三代:年幼代(younggeneration)、年长代(oldgeneration)和永久代(permanentgeneration)。从逻辑上讲,年幼代和年长代共同构成了Java堆,而永久代则被称为方法区(methodarea)。除了一些大对象可能在年长区中直接分配外,大部分对象都在年幼区中创建;而年长区除了那些直接创建的大对象外,大部分对象都是在年幼区中历经几次垃圾收集而幸免于难后被提升过来的。永久代中则保存着已载入类型的相关信息,包含了类、方法和其他一些内部使用的元数据,所有这些信息同样以对象的形式来组织和表示,虽然这些对象并不是Java对象,但是它们却象Java对象一样可以被同样的垃圾收集器所收集;另外,java.lang.String类所管理的内在化的字符串缓存池也在该代中分配;虽然名字叫做“永久”代,但其中的对象并不是永久的,只是沿用了历史名称而已。年幼代由一个伊甸区(EdenSpace)和两个更小的生还区(SurvivorSpace)组成,如下图所示(该图为缩略图,请点击察看原图)。大部分对象在伊甸区中创建,少数大对象可能在年长代中直接创建。生还区中保存的对象是历经一次或多次对年幼代的垃圾收集而未死的幸存者,并且在被认为已足够成熟而提升到年长代之前,它们仍有在稍后的某次垃圾收集过程中牺牲的可能。除非处在垃圾收集过程当中,两个生还区中只有一个用来容纳这些幸存者,另一个则保持为空。二、垃圾收集类型年幼代填满后,一次只针对该代的收集被执行,这样的收集也被称作“次收集(minorcollection)”。当年长代或永久代被填满后,一次针对所有代的完整收集被执行,这样的收集也被称作“主收集(majorcollection)”。通常来说,在一次主收集过程中,年幼代首先被收集,收集算法采用当前收集器的年幼代收集算法,该算法往往是针对年幼对象的行为特征而专门设计的;然后是对年长代和永久代的收集,收集算法都采用当前收集器的年长代收集算法。对于给定收集器所具体使用的年幼代和年长代收集算法,请参考下文。另外,主收集过程中如果存在压缩,则每代独自进行。不过,首先收集年幼代的策略在年长代空闲空间太小时会失效,因为年长代已无足够的空间来接纳所有的可能从年幼代提升过来的对象;在这种情况下,除CMS外的所有收集器都会放弃原有的年幼代收集算法,转而统一采用年长代收集算法对所有代进行收集。(CMS收集器之所以例外是因为它的年长代算法不能用来收集年幼代。)三、快速分配从下文对垃圾收集器的描述中可以看出,在许多情况下,内存中都有大块的连续空闲空间用以满足对象的分配请求。这种情形下的分配操作使用简单的“bump-the-pointer”技术,效率很高。按照这种技术,JVM内部维护一个指针(allocatedTail),它始终指向先前已分配对象的尾部,当新的对象分配请求到来时,只需检查代中剩余空间(从allocatedTail到代尾geneTail)是否足以容纳该对象,并在“是”的情况下更新allocatedTail指针并初始化对象。下面的伪代码具体展示了从连续内存块中分配对象时分配操作的简洁性和高效性:void*malloc(intn
本文标题:JVM中的垃圾收集
链接地址:https://www.777doc.com/doc-3185558 .html