paulwong

HotSpot 的垃圾收集 - 转

从J2SE 5.0开始,HotSpot JVM共包含四种垃圾收集器,它们全部基于分代算法。
一、代的划分
HotSpot JVM中内存被划分为三代:年幼代(young generation)、年长代(old generation)和永久代(permanent generation)。从逻辑上讲,年幼代和年长代共同构成了Java堆,而永久代则被称为方法区(method area)。除了一些大对象可能在年长区中直接分配外,大部分对象都在年幼区中创建;而年长区除了那些直接创建的大对象外,大部分对象都是在年幼区中历经几次垃圾收集而幸免于难后被提升过来的。永久代中则保存着已载入类型的相关信息,包含了类、方法和其他一些内部使用的元数据,所有这些信息同样以对象的形式来组织和表示,虽然这些对象并不是Java对象,但是它们却象Java对象一样可以被同样的垃圾收集器所收集;另外,java.lang.String类所管理的内在化的字符串缓存池也在该代中分配;虽然名字叫做“永久”代,但其中的对象并不是永久的,只是沿用了历史名称而已。
年幼代由一个伊甸区(Eden Space)和两个更小的生还区(Survivor Space)组成,如下图所示(该图为缩略图,请点击察看原图)。大部分对象在伊甸区中创建,少数大对象可能在年长代中直接创建。生还区中保存的对象是历经一次或多次对年幼代的垃圾收集而未死的幸存者,并且在被认为已足够成熟而提升到年长代之前,它们仍有在稍后的某次垃圾收集过程中牺牲的可能。除非处在垃圾收集过程当中,两个生还区中只有一个用来容纳这些幸存者,另一个则保持为空。
JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s5.sinaimg.cn/bmiddle/51501580g8169fb66ab44&690">

二、垃圾收集类型
年幼代填满后,一次只针对该代的收集被执行,这样的收集也被称作“次收集(minor collection)”。当年长代或永久代被填满后,一次针对所有代的完整收集被执行,这样的收集也被称作“主收集(major collection)”。通常来说,在一次主收集过程中,年幼代首先被收集,收集算法采用当前收集器的年幼代收集算法,该算法往往是针对年幼对象的行为特征而专门设计的;然后是对年长代和永久代的收集,收集算法都采用当前收集器的年长代收集算法。对于给定收集器所具体使用的年幼代和年长代收集算法,请参考下文。另外,主收集过程中如果存在压缩,则每代独自进行。
不过,首先收集年幼代的策略在年长代空闲空间太小时会失效,因为年长代已无足够的空间来接纳所有的可能从年幼代提升过来的对象;在这种情况下,除CMS外的所有收集器都会放弃原有的年幼代收集算法,转而统一采用年长代收集算法对所有代进行收集。(CMS收集器之所以例外是因为它的年长代算法不能用来收集年幼代。)

三、快速分配
从下文对垃圾收集器的描述中可以看出,在许多情况下,内存中都有大块的连续空闲空间用以满足对象的分配请求。这种情形下的分配操作使用简单的“bump-the-pointer”技术,效率很高。按照这种技术,JVM内部维护一个指针(allocatedTail),它始终指向先前已分配对象的尾部,当新的对象分配请求到来时,只需检查代中剩余空间(从allocatedTail到代尾geneTail)是否足以容纳该对象,并在“是”的情况下更新allocatedTail指针并初始化对象。下面的伪代码具体展示了从连续内存块中分配对象时分配操作的简洁性和高效性:
void * malloc(int n){
if( geneTail - allocatedTail < n )
doGarbageCollection();
void * wasAllocatedTail = allocatedTail;
allocatedTail += n;
return wasAllocatedTail;
}
对于多线程应用,分配操作必须是线程安全的。如果使用全局锁为此提供保证,则分配操作必定成为一个性能瓶颈。基于此,HotSport JVM采用了一种被称为“线程局部分配缓冲区”(Thread-Local Allocation Buffers,TLAB)的技术。该项技术为每个线程提供一个独立的分配缓冲区(伊甸区的一小部分),借此来提高分配操作的吞吐量。因为针对每个TLAB,只有一个线程从中分配对象,故而分配操作可以使用“bump-the-pointer”技术快速完成,而不必使用任何锁机制;只有当线程将其已有TLAB填满并且需要获取一个新的TLAB时,同步才是必须的。同时,为了减少TLAB所带来的空间消耗,还使用了一些其他技术,例如,分配器能够把TLAB的平均大小限制在伊甸区的1%以下。
“bump-the-pointer”和TLAB技术的组合保证了分配操作的高效性,类似new Object()这样的操作在大部分时间内只需要大约10条机器指令即可完成。

四、收集方式
1)串行(serial)和并行(parallel)
串行和并行是从收集任务本身如何被完成的角度来描述收集过程的。采用串行方式收集时,同一时间只有一件事情会发生。例如,即使有多个CPU可用,还是只有一个被用来执行收集任务。当采用并行方式收集时,垃圾收集任务被分成许多子任务,并且那些子任务在不同的CPU上被同时执行。同时操作使收集任务得以更快地完成,代价是增加了任务的复杂性并可能产生内存碎片。2)STW(stop-the-world)和并发(concurrent)
STW和并发是从收集任务是否影响应用程序执行的角度来描述收集过程的。当垃圾收集使用STW方式进行时,在收集期间应用程序将被完全挂起。作为另一种选择,收集任务可以采取并发方式,与应用程序一起同时执行。比较典型的情况是,并发收集器采取并发方式完成大部分工作,但也可能偶尔地不得不切换到STW方式,以完成一些需要应用程序短暂停顿的工作。STW收集比并发收集更为简单,因为在收集期间堆被冻结、对象不会改变;它的缺点是对某些应用程序来说,被暂停过久是不符合要求的。相对地,采用并发方式进行垃圾收集时,停顿时间会更短,但这也要求收集器必须更加小心,因为它正在操作可能被应用程序同时更新的对象。对并发收集器来说,这会增加额外开销从而影响性能,也会对堆空间产生更大的需求。

五、串行收集器(serial collector)
使用串行收集器时,对年幼代和年长代的收集都采用串行、STW方式进行。也就是说收集任务同时只使用一个CPU,而且在收集任务执行期间,应用程序的执行被中止。
1)串行收集器如何收集年幼代?
下图展示了使用串行收集器对年幼代进行收集时的操作细节。伊甸区中的活动对象被拷贝到初始为空的生还区2中;已占用的生还区1中的活动对象如果仍显年轻,则同样被拷贝到生还区2中,否则被直接拷贝到年长代;需要注意的是,生还区2一旦被填满,则所有尚未被拷贝的活动对象,不论其来自伊甸区还是生还区1,也不论其曾经幸免于多少次次收集,统统被拷贝到年长代。按照定义,在活动对象被拷贝之后,伊甸区和生还区1中的对象就全部成为垃圾对象,无须再被检查。(垃圾对象在图中以“X”标记,虽然实际上收集器并不检查和标记这些对象。) JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s4.sinaimg.cn/middle/51501580g81cfe5708d93&690"> 收集完成后,伊甸区和生还区1变为空闲空间,活动对象保存在生还区2中。此时,两个生还区的角色已经发生了互换。如下图:JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s4.sinaimg.cn/middle/51501580g81d362167ce3&690"> 2)串行收集器如何收集年长代?
串行收集器采用标记-清理-压缩算法收集年长代和永久代。在标记阶段,收集器遍历引用树,找出所有活动对象并打上标记;在清理阶段,顺序扫描代空间中所有对象(不论死活),计算出每个活动对象在代空间中的新位置;在压缩阶段,指向活动对象的所有引用被先期更新后,所有活动对象也被逐个滑动到其新的位置。由于所有活动对象都是按照次序朝代空间头部移动的,因此就在代空间尾部自然形成了一个单一而连续的空闲空间。如下图:JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s3.sinaimg.cn/middle/51501580g81fd1aa883d2&690">压缩过程使基于年长代或永久代的分配操作可以使用快速的“bump-the-pointer”技术。
3)何时使用串行收集器?
对于运行在客户端级硬件上并且对停顿时间没有特别要求的大多数应用而言,串行收集器都是首选。按照目前的硬件水平,串行收集器可以高效地管理使用64MB堆空间、最长停顿时间不能超过半秒的很多重要应用。
4)串行收集器的选用
在J2SE 5.0版本中,对于非服务器级硬件而言,串行收集器作为缺省的垃圾收集器被自动选用;对于其他硬件平台,则可以通过命令行选项“-XX:+UseSerialGC”进行显示的选用。

六、并行收集器(parallel collector)
目前,许多Java应用的运行平台大都包含很多物理内存和多个CPU。并行收集器,也被称作吞吐量收集器,被开发出来的主要目的就是为了充分利用CPU资源,而不是只让一个CPU去做垃圾收集而其他CPU却被闲置。
1)并行收集器如何收集年幼代?
和串行收集器相比,并行收集器采用了大致相同的年幼代收集算法,只是执行的是其并行版本而已。对年幼代的收集虽然仍基于拷贝技术、采用STW方式进行,但收集工作是并行展开的,使用了多个CPU,这就降低了垃圾收集开销,从而提高了应用程序的吞吐量。下图展示了并行收集器和串行收集器在执行年幼代收集时到底有何不同:
JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s7.sinaimg.cn/middle/51501580g81e435e495a6&690">
2)并行收集器如何收集年长代?
和串行收集器一样,并行收集器对年长代的收集同样基于标记-清理-压缩算法,同样采用串行、STW方式进行。
3)何时使用并行收集器?
能够得益于并行收集器的应用程序,必定运行在多CPU机器上,并且对停顿时间不能有特别的约束。因为可能持续时间很长的年长代收集虽然稀少,但还是会发生的。适于采用并行收集器的典型应用包括批处理、记帐、工资单和科学计算等等。你可能更倾向于选择并行压缩收集器(见下文)而不是并行收集器,因为前者对所有代(而不只是年幼代)的收集都采用并行方式进行。
4)并行收集器的选用
在J2SE 5.0版本中,对于服务器级硬件而言,并行收集器作为缺省的垃圾收集器被自动选用;对于其他硬件平台,则可以通过命令行选项“-XX:+UseParallelGC”进行显示的选用。

七、并行压缩收集器(parallel compacting collector)
并行压缩收集器在J2SE 5.0 U6中引入,它和并行收集器的区别在于,对年长代的收集它使用了全新的算法。注意:并行压缩收集器终将取代并行收集器。
1)并行压缩收集器如何收集年幼代?
同并行收集器,不再赘述。
2)并行压缩收集器如何收集年长代?
使用并行压缩收集器时,对年长代和永久代的收集都采用带滑动压缩的准并行、STW方式进行。为了满足并行处理的要求,每一个代空间均被逻辑划分为诸多定长区域(fixed-sized region),每个区域的相关信息保存在收集器维护的内部数据结构中。收集过程被分为标记、汇总和压缩三个阶段进行。在标记阶段,根引用集被划分给多个垃圾收集线程,它们同时运行,以并行的方式对活动对象进行追踪和标记;在活动对象被标记的同时,该对象的起始区域的数据也将被同步更新以反映该活动对象的大小和位置信息。
在汇总阶段(summary phase),操作不再基于对象,而是区域。考虑到先前收集过程中的压缩累积效应,每一个代空间中位于左侧的某一部分通常是密集的,主要包含了活动对象。从这样的密集区块中可能回收的空间数量使得它们并不值得被压缩。所以汇总阶段的首要任务就是检查区域的密集度,从最左边一个区域开始,直到找到这样的一个区域,使得在该区域及其右侧所有区域中可被回收的空间数量抵得上对它们进行压缩的成本。该区域左侧的所有区域就被称为密集前置区块,没有对象会被移入其中。该区域及其右侧所有区域会被压缩,以消除所有死区。汇总阶段的下一个任务就是计算并保存每个被压缩区域中活动数据的首字节在压缩后的新位置。需要注意的是:汇总阶段在目前被实现为一个串行阶段,这也是“准”并行方式的由来;并行实现也是可能的,只是与标记和压缩阶段的并行化相比,它对性能的影响不大。
在压缩阶段,垃圾收集线程使用汇总数据确定需要被填充的区域,然后它们就可以独立地把对象拷贝到这些区域中而不再需要额外的同步。这就产生了一个堆,堆空间的一端塞满了活动对象,而另一端则是一个单一而连续的空闲内存块。
3)何时使用并行压缩收集器?
和并行收集器一样,并行压缩收集器同样有益于在多CPU机器上运行的应用程序。除此之外,年长代收集的并行化操作方式还减少了停顿时间,使得并行压缩收集器比并行收集器更为适合那些有停顿时间限制的应用。不过,对于运行在大型共享主机(如SunRays)上的应用来说,并行压缩收集器也许并不太合适,因为任何单一应用都不应长时间独占几个CPU。在这样的机器上,要么考虑通过命令行选项“-XX:ParallelGCThreads=n”减少垃圾收集线程的数目,要么考虑选择一种不同的收集器。
4)并行压缩收集器的选用
并行压缩收集器只能通过命令行选项“-XX:+UseParallelOldGC”进行显示的选用。

八、并发的标记-清理收集器(Concurrent Mark-Sweep(CMS) Collector)
对于许多应用来说,端到端的吞吐量并不象响应时间那么重要。通常来讲,对年幼代的收集并不会引起太长时间的停顿。但是对年长代的收集,虽然不常发生,却可能导致停顿时间过长的状况,在堆空间很大时尤其明显。为了解决这个问题,HotSpot JVM包含了一个名叫“并发的标记-清理(CMS)收集器”的收集器,它也被称为低延迟收集器。
1)CMS收集器如何收集年幼代?
同并行收集器,不再赘述。
2)CMS收集器如何收集年长代?
采用CMS收集器收集年长代时,大部分收集任务与应用程序并发执行。
CMS收集器的收集周期始于初始标记,它采用串行、STW方式进行,用于确定根引用集。随后进入并发标记阶段,完成对所有活动对象的追踪和标记,在JDK6中该阶段已开始采用并行、并发方式进行。由于在并发标记过程中应用程序正在执行并可能更新了一些对象的引用域,因此并发标记过程结束时并非所有活动对象都已确保被标记出来。为了处理这种情况,应用程序再次暂停,收集过程进入再标记阶段;它采用并行、STW方式进行,通过对并发标记过程中被修改对象的再次访问最终完成整个标记过程。因为再标记阶段的停顿时间往往是最长的(超过初始标记停顿甚至次收集停顿),因此再标记过程会尽量采用并行方式进行。
再标记阶段完成后,所有活动对象都已确保被标记,随后进入并发清理阶段,它采用串行、并发方式进行,就地回收所有垃圾对象。下图展示了串行的标记-清理-压缩收集器和CMS收集器在收集年长代时的区别: JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s13.sinaimg.cn/middle/51501580g82676852d74c&690"> 因为一些任务(例如再标记过程中对被修改对象的再次访问)增加了收集器的工作量,CMS收集器的总体开销自然会增大。对于大多数试图减少停顿时间的收集器来说,这是一种典型的折衷。
CMS收集器是唯一一个不使用压缩技术的收集器。也就是说,在垃圾对象所占用的空间被释放以后,收集器并不把活动对象全部推挤到代空间的某一端去。见下图:JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s3.sinaimg.cn/middle/51501580g82716466bed2&690">这种方式节省了回收时间,但却因为空闲空间不再连续,收集器也就不再可能只使用一个简单指针即可指示出可分配给新对象的下一个空闲空间的位置,相反,它现在需要使用空闲空间列表。也就是说,收集器创建并通过一组列表把内存中尚未分配的区域连接起来,每当有对象需要分配空间时,适当的列表(基于所需内存数量)被搜索,以找到一块足以放下该对象的空闲区域。作为结果,与使用“bump-the-pointer”技术时相比,年长代中的分配操作变得更加昂贵。同时这也给年幼代收集带来了额外的开销,因为在其收集过程中每提升一个对象都会触发一次年长代中的分配操作。
CMS收集器的另一个缺点是和其他收集器相比它需要更大的堆空间。一方面,由于在标记阶段应用程序被允许运行,它就可能继续分配内存,从而可能使年长代空间不断地增长;另一方面,虽然标记阶段完成后所有活动对象都已确保被标记,但是在标记过程中一些对象却可能变为垃圾对象,而且直到下次年长代收集之前它们不会被回收。这样的对象也被称为游浮垃圾。
CMS收集器的最后一个缺点是由于缺乏压缩它可能引发碎片化问题。为了对付碎片化,CMS收集器跟踪对象的流行尺寸,预估未来需求,并为满足需求还可能分割或合并空闲内存块。
不象其他收集器,CMS收集器并不是等到年长代填满后才启动对年长代的收集,而是尝试尽早启动年长代收集,以便在年长代被填满之前收集过程可以完成。否则的话,CMS收集器将重新采用在串行和并行收集器中使用的标记-清理-压缩算法,尽管该算法工作于STW方式,也更加耗时。为避免这种情况的发生,CMS收集器对先前收集所耗时间和代空间充满所耗时间进行统计,并据此确定收集启动时间。另外,当年长代的空间占用率超过启动占用率(initiating occupancy)时,CMS收集器也将启动一次收集。启动占用率的值可以通过命令行选项“-XX:CMSInitiatingOccupancyFraction=n”进行设定,其中 n 表示年长代空间大小的百分比。缺省值为68。
总的来说,与并行收集器相比,CMS收集器(有时甚至显著地)减少了年长代收集的停顿时间,而代价是略有增加的年幼代收集的停顿时间、吞吐量方面的一些损失和额外的堆空间需求。
3)增量模式
CMS收集器可以采用让并发阶段增量完成的模式运行。这种模式通过对并发阶段的周期性暂停把处理能力交还给应用程序,以减少并发阶段持续时间过长所带来的不利影响。收集工作被分成许多小的时间块,它们在年幼代收集的间歇期被调度。当应用程序既需要CMS收集器提供的低停顿时间,又只能在很少的CPU(比如说1到2个)上运行时,这个特性就相当有用。
4)何时使用CMS收集器?
如果应用程序需要更短的垃圾收集停顿时间并且能够承担在运行时和垃圾收集器共享处理器资源,那么就可以使用CMS收集器。(由于其并发性,在垃圾收集过程中CMS收集器将和应用程序抢夺CPU周期。)通常来说,具有较大的长寿数据集并且运行在2个或多个CPU上的应用程序,更容易受益于CMS收集器的使用。一个典型的例子就是Web服务器。对于任何需要低停顿时间的应用程序来说,CMS收集器都值得考虑。对于年长代尺寸适中并且运行在单一处理器上的交互式应用程序来说,使用CMS收集器同样可能取得不错的效果。
5)CMS收集器的选用
CMS收集器只能通过命令行选项“-XX:+UseConcMarkSweepGC”进行显示的选用。如果希望CMS收集器在增量模式下运行,还需要通过命令行选项“-XX:+CMSIncrementalMode”启用该模式。

九、收集器、堆尺寸和虚拟机的自动选择
在J2SE 5.0中,根据应用程序所运行的硬件平台和操作系统,垃圾收集器、堆尺寸和HotSpot虚拟机(客户机或服务器)的缺省值被自动选定。这些自动的选择不仅减少了对命令行选项的使用,而且还更好的满足了不同类型应用程序的需要。
1)服务器级硬件(server-class machine,不使用“机器”这个词是因为读起来太拗口)的定义:
服务器级硬件必须同时满足以下两个条件:
①拥有2个或以上的物理处理器
②拥有2GB或以上的物理内存
该定义适用于所有平台,32位Windows平台除外。
2)服务器级硬件与非服务器级硬件下各项缺省值的比较:
机器类型 虚拟机 垃圾收集器 堆尺寸初始值-Xms 堆尺寸最大值-Xmx
服务器级硬件 服务器版 并行收集器 物理内存的1/64,不超过1GB 物理内存的1/4,不超过1GB
非服务器级硬件 客户机版 串行收集器 4MB 64MB
注意:本节所涉及的堆尺寸指的是Java堆的大小,包括年幼代和年长代,但不包括永久代。  

posted on 2011-10-30 20:47 paulwong 阅读(326) 评论(0)  编辑  收藏 所属分类: 性能优化


只有注册用户登录后才能发表评论。


网站导航: