HotSpot JVM的gcWord格式.docx
《HotSpot JVM的gcWord格式.docx》由会员分享,可在线阅读,更多相关《HotSpot JVM的gcWord格式.docx(22页珍藏版)》请在冰豆网上搜索。
![HotSpot JVM的gcWord格式.docx](https://file1.bdocx.com/fileroot1/2023-2/1/c1b44fd7-035f-4003-adbc-462a64565fbc/c1b44fd7-035f-4003-adbc-462a64565fbc1.gif)
这个过程在一些程序语言中是由程序员控制的,然而在这种复杂的工作中经常会发生一些错误,这些错误会导致不可预计的行为或者直接导致程序的崩溃,所以开发者的大量时间花费在调试和纠正这种错误上。
在手动内存管理的语言中经常遇到的问题就是悬挂引用(danglingrefrences)。
含义是一个对象被其他对象引用着,但是它占用的内存却已经被释放掉了,那么如果引用者访问被引用对象并且那个空间已经分配给了新的对象时,结果就是不可预知的。
手动管理内存的另一个常见问题是空间泄漏(spaceleaks)。
这种泄漏发生在不能释放掉已经分配但不再使用的内存。
例如,当你释放链表的空间时,恰巧释放在第一个元素占用的空间的时候发生了错误,那么链表中其他元素就不再被引用了,程序也无法引用他们,所以它们既不能被使用也不能被覆盖。
如果发生了很多这样的泄漏,那么所有的可用内存都可能被消耗掉。
另一种管理内存的方法是自动管理,这种方法很常见,尤其是在现代的面向对象的语言中。
自动管理使用了一种叫做内存垃圾收集器(garbagecollector)的程序。
自动的内存管理可以提高代码的抽象度和可靠性。
垃圾收集器避免了悬挂指针(danglingreference),原因是一个仍然被引用的对象永远不会内存回收并且也不会被认为已经被释放掉了。
垃圾收集器解决了空间泄漏(spaceleaks)问题,原因是它可以自动释放不再被引用的空间。
3内存收集的概念(GarbageCollectionConcepts)
一个内存收集器具有下面的功能:
●分配内存
●保证所有被引用的对象还在内存中
●可以释放在运行的代码中不再引用的对象的内存
如果对象被引用着,那我们说它活着(live);
如果对象不再被引用了,那我们说他死了(dead),术语称作垃圾(garbage)。
寻找并释放这些对象的空间的过程就做垃圾收集(garbagecollection)。
垃圾收集解决了内存分配的很多问题但不是所有问题。
例如,你可以无限期的创建对象并保持对他们的引用,直到内存耗尽。
垃圾收集本身也是一个复杂的工作,需要消耗时间和资源。
垃圾收集器使用了一种精确的算法用于组织内存、分配与释放空间,并且这种算法对于编程人员来说是透明的。
被分配的空间来自于被称为堆(heap)的大块的内存池。
那么在什么时候会出发垃圾收集动作呢?
一般来说整个堆或一部分被填满或者达到某一百分比数值时将被收集。
为了满足一个内存分配的请求,必须要在堆中找到一块特定尺寸的没有被使用的内存,然而这是一个艰难的任务。
大多数动态内存分配算法的主要问题是避免碎片(fragmentation),这样可以使内存的分配与释放更有效率。
理想的垃圾收集器的特点(DesirableGarbageCollectorCharacteristics)
一个垃圾收集器必须是即安全又足够聪明的。
也就是说,存活的数据一定不能被释放掉,并且垃圾对象应该在尽量少的收集周期中被释放掉。
垃圾对象即使出现了一定规模的循环引用也一定要释放掉。
理想的垃圾收集器的操作是非常高效的,不会引起长暂停(在这段时间里应用程序是无法运行的)。
尽管如此,像大多数与计算相关的系统一样,需要在时间、空间和频率保持平衡。
例如,如果堆比较小,那么垃圾收集会很快但是堆也会很快被填满,所以收集的频率会更高。
相反,一个大体积的堆需要花费更长的时间才能填满,所以会大大降低回收频率,但是每次回收的时间会更长。
理想的垃圾收集器的另一个特点是对存储碎片的限制。
当垃圾对象被释放时,释放的空间将以小的字节形式存在于各个领域,因此,当为大的目标分配空间时,任何一个邻近领域中的已释放空间都不够大。
一种清除碎片的方法是压缩法(Compaction),下节讲述设计选择时将会详细介绍压缩法。
可收缩性(Scalability)也很重要。
在多处理器系统中多线程应用上,分配空间不应该成为可收缩性的瓶颈,并且收集也不应该成为一个瓶颈。
设计选择(DesignChoices)
当设计或者选择垃圾收集器的算法时,需要做许多选择。
串行VS.并行(SerialversusParallel)
对于串行收集,一次只会有一件事会发生。
例如,即使当多CPU体系可以应用时,只有一个会被用来执行收集任务。
当使用并行收集时,垃圾收集工作被分成几部分,这些子部将会在不同的CPU上被同时执行。
同时执行会使垃圾收集得更快,但是代价是会增加复杂性和潜在碎片。
并发VS.完全停顿(ConcurrentversusStop-the-world)
当执行停顿垃圾收集(stop-the-worldgarbagecollection)时,应用程序会被完全挂起。
相反,一个或者多个垃圾收集任务也可以并发的与应用程序同时执行。
通常,一个并发收集器可以并发的执行垃圾收集的大部分工作,但是也会不可避免的引发一个小的停顿。
停顿垃圾收集要比并行的垃圾收集器简单,因为收集期间的堆是冻结的,其中的对象不会发生变化;
它的缺点是应用程序一定会发生停顿。
相应的,如果并发的执行垃圾收集则暂停时间会更短,但是内存收集器必须更加小心,因为在其工作期间一个对象可能已经被应用程序修改过了;
这种额外的负担影响了并发收集器的性能并且要求一个大尺寸的堆。
压缩VS.非压缩VS.拷贝
在垃圾收集器判断完存储器中有用的数据和垃圾数据后,垃圾收集器会压缩存储器,将有用的数据移到一起,而剩下的存储空间被全部回收。
压缩结束后,在释放的地址上分配一个新的目标是非常简单快速的。
可以用一个简单的指针指向下一个未分配的空间。
与压缩收搜集器相比,非压缩收集器用占位(in-place)垃圾目标来释放空间,与压缩收集器不同,非压缩收集器不通过移动所有有用的数据来创造一个大的再回收空间。
非压缩收集器的优点是可以快速进行垃圾收集,缺点是存在潜在碎片。
通常,用在适当位置(in-place)再分配一个堆比用压缩法更为昂贵。
在存储器邻近区域寻找一个有足够空间来分配新目标的堆是很有意义的。
第三个选择是拷贝收集器,它拷贝有用的数据到不同的存储地址。
这样它的资源空间就可以被认为是可分配的,并且可以快速容易的分配,但是拷贝收集器的缺点是需要拷贝数据的额外的时间和空间。
性能指标
垃圾收集器的性能指标如下:
1.吞吐量(Throughput):
没有花费在垃圾回收的时间占总执行时间的百分比。
2.逆吞吐量(Garbagecollectionoverhead):
垃圾收集的时间所占总执行时间的百分比。
3.暂停时间(Pausetime):
当垃圾收集器执行时,应用程序停止运行的时间。
4.收集频率(Frequencyofcollection):
收集器工作的频度,与应用程序运行情况有关。
5.印迹(Footprint):
一种度量方式,就像堆的大小。
6.及时性(Promptness):
当一个目标变成垃圾后,多久这个存储空间会变成有用的。
一个交互式请求可能会要求低的暂停次数,然而总共的执行时间对于非交互式请求来说更重要;
一个实时的应用可能要求不能出现长时间的停顿又要平衡花费在垃圾收集的各个阶段的时间;
在小型个人微机或者嵌入系统中可能更加在意小的印迹。
世代收集器(GenerationalCollection)
使用世代收集器技术,存储器被分成几个代,就是用独立的池容纳不同年龄的目标。
例如,最广泛应用的方案有两个代:
一个为新对象,一个为就对象。
不同的代可以用不同的算法来执行垃圾收集,每个算法根据不同代的不同测试结果进行优化。
世代收集器会开发如下的测试,被称作弱世代假说(Weakgenerationalhypothesis),相关的应用采用几种程序语言编写,包括Java程序语言:
1.大多数分配目标长时间不被引用,也就是,它们夭折了。
2.从老一代到新生代目标的引用很少存在。
新生代收集器的发生频率与效率都很高,因为新生代空间通常较小并且可能容纳许多不再被引用的目标。
新生代收集器中存活的对象最终会进入旧生代,见图片1。
旧生代比新生代明显大,并且内存的消耗速度更慢。
结果,旧生代收集器很少执行,并且需要更长的时间来完成。
因为新生代发生垃圾收集的频度分厂高,所以在考虑为它选择收集算法的时候需要特别考虑收集速度;
另外,由于旧生代占用的堆空间是最大的并且必须在低垃圾密度下很好的工作,所以它的算法更侧重与空间的使用效率。
4在J2SE5.0HotSpot虚拟机中的垃圾收集器(GarbageCollectorsintheJ2SE5.0HotSpotJVM)
TheJavaHotSpotvirtual
J2SE5.0update6中JVM包含四种垃圾收集器,都是基于代的,本节描述了代与其他类型的组合。
并针对每个收集器为什么可以快速和有效的分配对象而给出了详细的资料。
HotSpotGenerations
在JVM中内存被分为三代:
新生代(YoungGeneration)、旧生代(OldGeneration)和持久代(PermanentGeneration)。
其中新生代一般用于放置新创建的对象,而旧生代则将一些经过几次垃圾回收仍然没有被回收的对象转移存放进来。
持久代用于放置一些为JVM本身的方便性而使用的对象,比如类定义、方法定义、常量、代码段等。
新生代是由一个Eden区和两个survivor区组成。
如图2。
大多数的对象是在Eden区生成的,(正如上文所说,少数对象可能会直接分配到旧生代里)。
当一个survivor区满时,有些对象经过至少一个回收周期仍然未被回收将要进入旧生代之前,将被存放到另一个标记为空的survivor区内,等待下一次的垃圾回收。
GarbageCollectionTypes(垃圾收集的类型)
当新生代满时,自身范围内会做一个代内收集(有时称为轻微收集)。
当旧生代或者持久代满时,将会做一次完整的收集(有时称为大规模收集),也就是说将会收集所有的代。
首先收集新生代,有专门针对新生代的算法,它在新生代里可以高效的回收垃圾。
然后是执行可以收集旧生代和持久代的算法。
如果需要压缩,那么将会分开压缩每一个代。
当旧生代满时,如果新生代先收集,有些对象可能将要从新生代转入到旧生代,在这种情况下,在CMS收集器中新生代里的算法是不会执行的,而是旧生代的算法针对整个堆(CMS旧生代算法比较特殊,它无法收集新生代)。
FastAllocation(快速分配)
你将会在下面看到垃圾收集器的说明。
在许多情况下,内存会有大块的空间用来分配对象,使用了bump-the-pointer技术使得这些变得更有效率,也就是说,要保存之前分配的对象的末地址,在余下代中检查是否有符合新分配的要求的对象,如果有将更新指针和初始化对象。
为多线程应用分配业务时,必须注意多线程安全。
如果使用全局锁将会成为瓶颈和降低性能,所以JVM使用了一种Thread-LocalAllocationBuffers(TLABs)技术,为每个线程提供自己的缓冲区(即代的一小部分)用来内存分配,从而提高多线程分配的吞吐量。
因为,只有一个线程分配于每个TLAB,分配可以利用高效的bump-the-pointer技术,而不需要任何锁定。
极少情况下,当一个线程完全被TLAB占用并且必须得到一个新的,可以同步执行,一些操作TLABs技巧可以减少空间的浪费。
比如,TLABs是平均分配尺寸耗费小于1%的Eden,在每个分配中结合使用TLABs与线性的使用bump-the-pointer技术的方式是高效的,只需要10个本地指令。
SerialCollector(串行收集器)
在串行收集器中只会使用一颗CPU进行垃圾回收,所以新生代和旧生代的回收一定是串行发生的,并且在垃圾回收期间应用程序是被挂起的。
使用串行收集器的新生代
Figure3展示了一次使用串行收集器回收新生代内存的过程。
存活在Eden中的对象被复制到了初始化为空的survivorspace(在图中被标记为To)中,除了那些由于太大而直接复制到了旧生代的对象;
存活在非空的survivorspace(在图中被标记为From)中的对象有两个出路,如果相对来说仍然是新对象那么也被复制到了初始化为空的survivorspace中,否则相对来说就是旧对象了,那么被复制到旧生代中。
注意:
如果To区域被填充满了,那么来自Eden和From区域的没有进入To区域的对象直接进入旧生代,无论这个数量有多大。
经过这次复制之后那些原本存活在Eden和From区域的对象就可以认为是死亡(dead)的了,它的空间可以被用来重分配(虽然回收器并没有检查或标记这些对象,但是他们确实已经是垃圾对象了,在图中被X标记)。
在一次新生代垃圾回收之后,原来的Eden和From区域都被清空了,只有To区域有对象,此时From与To区域完成了角色转换,见Figure4。
使用串行收集器的旧生代
在串行收集器中旧生代和持久代使用的是标记-清除-压缩(mark-sweep-compact)算法。
在标记阶段,收集器检查哪些对象是存活的;
在清除阶段,可以识别出哪些是垃圾对象;
然后进入压缩阶段,收集器将存活对象向旧生代区域开始的方向滑动(旧生代也是如此),那么另一端就会出现一个连续的大块的自由空间,见Figure5。
压缩后允许在旧生代和持久代中使用快速的bump-the-point技术分配内存。
什么时候使用串行收集器
串行收集器为那些以客户端(client-style)方式运行的和不要求低暂停时间(lowpausetimes)的应用提供了选择。
根据当今的硬件条件,串行收集器可以高效率的管理那些使用了64M堆空间的应用并且可以0.5s内完成完全收集,降低有害暂停(worst-casepauses)。
选择串行收集器
在release版本的J2SE5.0中,串行收集器在非服务器模式(server-class)的机器上是默认的垃圾回收策略;
当然也可以使用-XX:
UseSerialGC的命令行参数指明使用串行收集器。
ParallelCollector(并行收集器)
现在很多Java应用运行在大内存和多CPU的机器上。
并行收集器又称为吞吐量收集器,是为了高效使用可用CPU的策略,避免出现其他CPU空闲只有一颗CPU进行垃圾回收时的情景。
使用并行收集器的新生代
串行收集器对新生代的算法是串行收集器的并行版本。
也具有停顿(stop-the-world)和复制(copying)特征,但是由于使用了多CPU并行的收集新生代,所以降低了垃圾回收的耗时提高了应用的吞吐量。
Figure6反映了这种差别。
使用并行收集器的旧生代
此时的旧生代使用了与串行收集器相同的算法,即标记-清理-压缩(mark-sweep-compact)算法。
什么时候使用并行收集器
因为仍然会发生低频率的长耗时的旧生代垃圾回收动作,所以那些运行在多CPU机器上的并且对暂停时间(pausetime)不敏感的应用可以获得益处。
例如那些进行批量处理的应用,如账单计算、工资计算、科学计算等等。
你可能在考虑选择并行压缩的收集器(下面会讲到的内容)而不仅仅是并行收集器,因为前者可以在任何的代中使用并行收集而不仅是新生代。
选择并行收集器
在release版本的J2SE5.0中,并行收集器在服务器模式(server-class)的机器上是默认的垃圾回收策略;
UseParallelGC的命令行参数指明使用并行收集器。
ParallelCompactingCollector(并行压缩收集器)
并行压缩收集器是在J2SE5.0update6版本中被引入的,与并行收集器相比在对旧生代的收集上使用了新的算法。
最终,并行压缩收集器会取代并行收集器。
使用并行压缩收集器的新生代
与使用并行收集器的新生代相同。
使用并行压缩收集器的旧生代
在并行压缩收集器中,通常利用带有滑动压缩功能的并行方式(parallelfashionwithslidingcompaction)在同一个停顿(stop-the-world)中回收旧生代和持久代,共分三个阶段。
首先,在逻辑上将每个代分割成固定尺寸的区域。
在标记阶段(markingphase),可以通过应用代码直接到达的存活的对象集合(theinitialsetofliveobjects)被划分在垃圾收集器所有的线程(garbagecollectionthreads)中,然后所有的存活对象被并行的做了标记;
当一个对象被确定为存活的的时候,该对象的尺寸和位置会被用来更新它所在的区域的数据。
在总结摘要阶段(summaryphase)对区域的操作,经过了对前面的对象集合的压缩以后的典型结果是每个代的左边包含了很多的存活对象,所以密度变大了;
对这些密集的区域进行压缩来释放空间代价较高不划算;
所以在摘要阶段的第一件事情是检查每个区域的密度,从区域的最左边开始直到某个点(point),这个点是区域可分配的(couldberecoveredfromaregion)并且它的右边区域是值得压缩的;
这个点的所有的左边区域被使用密集前缀(denseprefix)标记起来,其中的对象不会发生移动;
这个点的右边所有区域会被压缩释放空间;
摘要阶段会计算和保存每个压缩区域中每个存货对象的第一个字节所在的新位置。
目前摘要阶段是以串行的方式实现的,虽然也可以采取并行的方式,但并不像在并行标记和并行压缩阶段(parallelizationofthemarkingandcompactionphases)那么重要。
在压缩阶段,收集器的线程们使用摘要信息判断某个区域是否应该被填充(压缩),并且可以独立的向某个区域复制数据;
经过了这个阶段,堆的一端密度非常大,另一端是一个大块的空白区域。
增量模式(IncrementalMode)
并发标记-清除收集器可以运行在增量的模式下运行,意思是说可以增量的完成并发阶段(concurrentphases)的工作;
这意味着可以降低长并发阶段后处理应用的消耗。
这项工作被分割成小的时间片分散在新生代的垃圾回收中。
这个功能对于那些CPU数量较少(例如1颗或2颗)并且需要在并发收集器中获得低暂停时间的应用。
关于这个模式的更多信息可以见《TuningGarbageCollectionwiththe5.0Java™VirtualMachine》的第九章节。
什么时候使用并行压缩收集器
和并行收集器一样,多CPU的应用可以从中受益。
另外,对旧生代的并行压缩可以减少暂停时间并且使并行压缩收集器比并行收集器更合理,而并行收集器的暂停时间对于应用来说是个瓶颈。
可能并行压缩收集器不适合运行在大的共享的机器上(例如SunRays),因为其上的每个应用都不应该独占多个CPU以便获得跟多的处理时间;
在这样的机器上我们可以减少垃圾回收的线程数(通过配置-XX:
ParalleGCThreads=n的命令行参数)或者选择其他的收集器方案。
选择并行压缩收集器
使用-XX:
UseParallelOldGC参数可以启用并行压缩收集器。
ConcurrentMark-Sweep(CMS)Collector(并发标记-清除收集器)
对于很多应用来说,快速的响应速度比端到端的吞吐量(end-to-endthroughput)更重要。
通常新生代的收集时间的不会引起长的暂停时间,但是旧生代的收集(虽然发生的频率不高)可能引起长暂停,尤其是对大尺寸堆栈的收集。
为了解决这个问题,HotSpotJVM引入了一种叫做并发标记-清除收集器(Concurrentmark-sweep简拼CMS),也叫做低反应时间收集器(low-latencycollector)。
使用并发标记-清除收集器的新生代
与并行收集器相同。
使用并发标记-清除收集器的旧生代
并发标记-清除收集器对于旧生代的收集的大部分时间是与应用程序并发执行的。
并发标记-清除收集器的生命周期是以一个叫做初始化标记(initialmark)的短暂停开始的;
这个过程标示出一组可以通过程序代码直接到达的存活的对象。
然后在并发标记(concurrentmarkphase)阶段,收集器将这些对象中的间接可到达(transitivelyreachable)的存活对象标记出来;
但是这个阶段应用程序也在运行并更新着引用对象,所以在这个阶段结束时并不是所有的存货对象都被发现并标记了;
为了解决这个问题,应用程序进入了第二次暂停,叫做重新标记(remark),期间通过重新访问所有在并发标记阶段修改过的对象完成标记工作,多线程的并行运行提高了效率。
在重新标记阶段的后期,堆中所有的存货对象都被检查到了并被做了标记,所以随后的并发清除阶段(subsequentconcurrentsweepphase)回收了所有的垃圾空间。
Figure7显示了使用串行标记-清除-压缩(serialmark-sweep-compact)收集器和并发标记-清除收集器的旧生代的差别。
由于一些不得不做的任务,比如在重新标记阶段的重新访问对象(remarkobjects),增加了收集器的消耗,所以这是一种典型的为了缩短暂停时间做的牺牲。
并发标记-清除收集器是唯一一种无压缩的