【深入理解JVM】探究垃圾收集器和内存分配

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wmjvHKi-1661031458236)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.001.png)]

经典垃圾收集器

如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。《Java虚拟机规 范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含 的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2LsHp1Y4-1661031458237)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.002.png)]

如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。

Serial收集器

Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新⽣代收集器的唯⼀选择。这个收集器是⼀个单线程⼯作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使⽤⼀个处理器或⼀条收集线程去完成垃圾收集⼯作,更重要的是强调在它进⾏垃圾收集时,必须暂停其他所有⼯作线程,直到它收集结束。(Stop The World)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qhL7IA6l-1661031458238)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.003.png)]

事实上,迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生 代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以 获得最高的单线程收集效率。所以, Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并⾏版本,除了同时使⽤多条线程进⾏垃圾收集之外,其余的⾏为包括Serial收集器可⽤的所有控制参数(例如: -XX: Survivor Ratio、 -XX: PretenureSizeThreshold、-XX: HandlePromotionFailure等)、收集算法、 Stop The World、对象分配规则、回收策略等都与Serial收集器完全⼀致,在实现上这两种收集器也共⽤了相当多的代码。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A5yKyfyE-1661031458238)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.004.png)]

ParNew收集器除了⽀持多线程并⾏收集之外,其他与Serial收集器相⽐并没有太多创新 之处,但它却是不少运⾏在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中⾸选的新⽣代收集器,其中有⼀个与功能、性能⽆关但其实很重要的原因是: 除了 Serial收集器外,⽬前只有它能与CMS收集器配合⼯作

在JDK 5中使⽤CMS来收集⽼年代的时候,新⽣代只能选择ParNew或者Serial收集器中 的⼀个。ParNew收集器是激活CMS后(使⽤-XX: +UseConcMarkSweepGC选项)的默认新⽣代收集器,也可以使⽤-XX: +/-UseParNewGC选项来强制指定或者禁⽤它。

所以⾃JDK 9开始,ParNew加CMS收集器的组合就不再是官⽅推荐的服务端模式下的收集器解决⽅案了。官⽅希望它能完全被G1所取代,甚⾄还取消了ParNew加Serial Old以及Serial加CMS这两组收集器组合的⽀持(其实原本也很少⼈这样使⽤),并直接取消了XX:+UseParNewGC参数,这意味着ParNew和CMS从此只能互相搭配使⽤,再也没有其他收集器能够和它们配合了。读者也可以理解为从此以后,ParNew合并⼊CMS,成为它专⻔处理新⽣代的组成部分

并行和并发的概念

并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线 程在协同工作,通常默认此时用户线程是处于等待状态

并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾 收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于 垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。

Parallel Scavenge收集器

Parallel Scavenge收集器也是⼀款新⽣代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并⾏收集的多线程收集器

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时⽤户线程的停顿时间,⽽Parallel Scavenge收集器的⽬标则是达到⼀个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器⽤于运⾏⽤户代码的时间与处理器总消耗时间的⽐值:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M7Lt03Kq-1661031458238)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.005.png)]

如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分 钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。

Parallel Scavenge收集器提供了两个参数⽤于精确控制吞吐量,分别是

控制最⼤垃圾收集停顿时间的-XX: MaxGCPauseMillis参数

直接设置吞吐量⼤⼩的-XX: GCTimeRatio 参数。

Parallel Scavenge收集器还有⼀个参数 -XX: +UseAdaptiveSizePolicy。这是⼀个开关参数,当这个参数被激活之后,就不需要⼈⼯指定新⽣代的⼤⼩(-Xmn)、Eden与Survivor区的⽐例( -XX: SurvivorRatio ) 、 晋升⽼年代对象⼤⼩( -XX : PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运⾏情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最⼤的吞吐量。⾃适应调节策略也是Parallel Scavenge收集器区别于 ParNew收集器的⼀个重要特性。

Serial Old收集器

Serial Old是Serial收集器的⽼年代版本,它同样是⼀个单线程收集器,使⽤标记-整理算 法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使⽤。如果在服务端模式下,它也可能有两种⽤途: ⼀种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使⽤,另外⼀种就是作为CMS收集器发⽣失败时的后备预案,在并发收集发⽣Concurrent Mode Failure时使⽤。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9VoHnzMh-1661031458239)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.006.png)]

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的⽼年代版本,⽀持多线程并⾏收集,基于标记 -整理算法实现。这个收集器是直到JDK 6时才开始提供的,在此之前,新⽣代的Parallel Scavenge收集器⼀直处于相当尴尬的状态,原因是如果新⽣代选择了Parallel Scavenge收集器,⽼年代除了Serial Old(PS MarkSweep)收集器以外别⽆选择,直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了⽐较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9tbVwVPG-1661031458239)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.007.png)]

CMS收集器

CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。 ⽬前很⼤⼀部分的Java应⽤集中在互联⽹⽹站或者基于浏览器的B/S系统的服务端上,这类应⽤通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给⽤户带来良好的交互体验。CMS收集器就⾮常符合这类应⽤的需求。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

1)初始标记(CMS initial mark) 需要STW

2)并发标记(CMS concurrent mark)

3)重新标记(CMS remark) 需要STW

4)并发清除(CMS concurrent sweep)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qXe4C4Gm-1661031458239)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.004.png)]

CMS优点:CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿,一些官方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector)。CMS收集器是 HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度。

CMS缺点:

  1. ⾸先,CMS收集器对处理器资源⾮常敏感。事实上,⾯向并发设计的程序都对处理器资 源⽐较敏感。在并发阶段,它虽然不会导致⽤户线程停顿,但却会因为占⽤了⼀部分线程 (或者说处理器的计算能⼒)⽽导致应⽤程序变慢,降低总吞吐量。
  2. 然后,由于CMS收集器⽆法处理“浮动垃圾”(FloatingGarbage),有可能出现“Concurrent Mode Failure”失败进⽽导致另⼀次完全“Stop The World”的Full GC的产⽣。 在CMS的并发标记和并发清理阶段,⽤户线程是还在继续运⾏的,程序在运⾏⾃然就还 会伴随有新的垃圾对象不断产⽣,但这⼀部分垃圾对象是出现在标记过程结束以后,CM S⽆法在当次收集中处理掉它们,只好留待下⼀次垃圾收集时再清理掉。这⼀部分垃圾就 称为“浮动垃圾”。同样也是由于在垃圾收集阶段⽤户线程还需要持续运⾏,那就还需要预 留⾜够内存空间提供给⽤户线程使⽤,因此CMS收集器不能像其他收集器那样等待到⽼ 年代⼏乎完全被填满了再进⾏收集,必须预留⼀部分空间供并发收集时的程序运作使⽤。 要是CMS运⾏期间预留的内存⽆法满⾜程序分配新对象的需要,就会出现⼀次“并发失 败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案: 冻结⽤户线程的 执⾏,临时启⽤Serial Old收集器来重新进⾏⽼年代的垃圾收集, 但这样停顿时间就很⻓ 了。所以参数-XX: CMSInitiatingOccupancyFraction设置得太⾼将会很容易导致⼤量的并 发失败产⽣,性能反⽽降低,⽤户应在⽣产环境中根据实际应⽤情况来权衡设置。
  3. 还有最后⼀个缺点,CMS是⼀款基于**“标记-清除”算法实现的收集器**,这意味着收集结束时会有⼤量空间碎⽚产⽣。空间碎⽚过多时,将会给⼤对象分配带来很⼤麻烦,往往会出 现⽼年代还有很多剩余空间,但就是⽆法找到⾜够⼤的连续空间来分配当前对象,⽽不得 不提前触发⼀次Full GC的情况。 因此虚拟机设计者们还提供了另外⼀个参数-XX: CM SFullGCsBefore- Compaction(此参数从JDK 9开始废弃),这个参数的作⽤是要求CMS收 集器在执⾏过若⼲次(数量由参数值决定)不整理空间的Full GC之后,下⼀次进⼊Full GC 前会先进⾏碎⽚整理(默认值为0,表示每次进⼊Full GC时都进⾏碎⽚整理)

Garbage First收集器

G1是⼀款主要⾯向服务端应⽤的垃圾收集器。HotSpot开发团队最初赋予它的期望是(在 ⽐较⻓ 期的)未来可以替换掉JDK 5中发布的CMS收集器。现在这个期望⽬标已经实现过半了,JDK 9发布之⽇,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,⽽CMS则沦落⾄被声明为不推荐使⽤(Deprecate)的收集器。

G1可以⾯向堆内存任何部分来组成回收集(Collection Set,⼀般简称CSet)进⾏回收,衡量标准不再是它属于哪个分代,⽽是哪块内存中存放的垃圾数量最多,回收收益最⼤,这就是G1收集器的Mixed GC模式。

G1开创的基于Region的堆内存布局是它能够实现这个⽬标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有⾮常明显的差异: G1不再坚持固定⼤⼩以及固定数量的分代区域划分,⽽是把连续的Java堆划分为多个⼤⼩相等的独⽴区域(Region),每⼀个Region都可以根据需要,扮演新⽣代的Eden空间、Survivor空间, 或者⽼年代空间。收集器能够对扮演不同⻆⾊的Region采⽤不同的策略去处理,这样⽆论是新创建的对象还是已经存活了⼀段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有⼀类特殊的Humongous区域,专⻔⽤来存储⼤对象。G1认为只要⼤⼩超过了⼀个Region容量⼀半的对象即可判定为⼤对象。每个Region的⼤⼩可以通过参数XX: G1HeapRegionSize设定,取值范围为1M B~32M B,且应为2的N次幂。⽽对于那些超过了整个Region容量的超级⼤对象, 将会被存放在N个连续的Humongous Region之中,G1的⼤多数⾏为都把Humongous Region作为⽼年代的⼀部分来进⾏看待。

虽然G1仍然保留新⽣代和⽼年代的概念,但新⽣代和⽼年代不再是固定的了,它们都是⼀系列区域(不需要连续)的动态集合。G1收集器之所以能建⽴可预测的停顿时间模型,是因为它将Region作为单次回收的最⼩单元,即每次收集到的内存空间都是Region⼤⼩的整数倍,这样可以有计划地避免在整个Java堆中进⾏全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region⾥⾯的垃圾堆积的“价值”⼤⼩,价值即回收所获得 的空间⼤⼩以及回收所需时间的经验值,然后在后台维护⼀个优先级列表,每次根据⽤户设定允许的收集停顿时间(使⽤参数-XX: MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最⼤的那些Region,这也就是“Garbage First”名字的由来。这种使⽤Region划分内存空间,以及具有优先级的区域回收⽅式,保证了G1收集器在有限的时间内获取尽可能⾼的收集效率。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UWHi3Vnd-1661031458239)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.008.png)]

一些G1的实现细节:

  1. **将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?**使⽤记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应⽤其实要复杂很多,它 的每个Region都维护有⾃⼰的记忆集,这些记忆集会记录下别的Region指向⾃⼰的指 针,并标记这些指针分别在哪些卡⻚的范围之内。G1的记忆集在存储结构的本质上是⼀ 种哈希表, Key是别的Region的起始地址,Value是⼀个集合,⾥⾯存储的元素是卡表的 索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)⽐原 来的卡表实现起来更复杂,同时由于Region数量⽐传统收集器的分代数量明显要多得 多,因此G1收集器要⽐其他的传统垃圾收集器有着更⾼的内存占⽤负担。根据经验,G1 ⾄少要耗费⼤约相当于Java堆容量10%⾄20%的额外内存来维持收集器⼯作。
  2. **在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?**这⾥⾸先要解决的是⽤户线程改变对象引⽤关系时,必须保证其不能打破原本的对象图结构,CMS收集器采⽤增 量更新算法实现,⽽G1收集器则是通过原始快照(SATB)算法来实现的。此外,垃圾收集 对⽤户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运⾏就肯定 会持续有新对象被创建,G1为每⼀个Region设计了两个名为TAMS(Top at Mark Start) 的指针,把Region中的⼀部分空间划分出来⽤于并发回收过程中的新对象分配,并发回 收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上 的对象是被隐式标记过的,即默认它们是存活的,不纳⼊回收范围。与CMS中的 “Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分 配的速度,G1收集器也要被迫冻结⽤户线程执⾏,导致Full GC⽽产⽣⻓时间“Stop The World”。
  3. **怎样建立起可靠的停顿预测模型?**G1收集器的停顿 预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记 录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得 出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易 受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句 话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由 哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

G1运作过程大概分为以下四个动作:

·初始标记(Initial Marking): 仅仅只是标记⼀下GC Roots能直接关联到的对象,并且修改 TAMS指针的值,让下⼀阶段⽤户线程并发运⾏时,能正确地在可⽤的Region中分配新对 象。这个阶段需要停顿线程,但耗时很短,⽽且是借⽤进⾏Minor GC的时候同步完成 的,所以G1收集器在这个阶段实际并没有额外的停顿。

·并发标记(Concurrent Marking): 从GC Root开始对堆中对象进⾏可达性分析,递归扫 描整个堆⾥的对象图,找出要回收的对象,这阶段耗时较⻓,但可与⽤户程序并发执⾏。 当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引⽤变动的对象。

·最终标记(Final Marking): 对⽤户线程做另⼀个短暂的暂停,⽤于处理并发阶段结束后仍 遗留下来的最后那少量的SATB记录。

·筛选回收(Live Data Counting and Evacuation): 负责更新Region的统计数据,对各个 Region的回收价值和成本进⾏排序,根据⽤户所期望的停顿时间来制定回收计划,可以 ⾃由选择任意多个Region构成回收集,然后把决定回收的那⼀部分Region的存活对象复 制到空的Region中,再清理掉整个旧Region的全部空间。这⾥的操作涉及存活对象的移 动,是必须暂停⽤户线程,由多条收集器线程并⾏完成的。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iwQc1Vwk-1661031458240)(Aspose.Words.0c9d5388-9a2a-4a5f-be6d-c3311374622a.009.png)]

相⽐CMS,G1的优点有很多,暂且不论可以指定最⼤停顿时间分Region的内存布局按收益动态确定回收集这些创新性设计带来的红利,单从最传统的算法理论上看,G1也更有发展潜⼒。与CMS 的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看⼜是基于“标记-复制”算法实现,⽆论如何,这两种算法都意味着G1运作期间不会产⽣内存空间碎⽚,垃圾收集完成之后能提供规整的可⽤内存。这种特性有利于程序⻓时间运⾏,在程序为⼤对象分配内存时不容易因⽆法找到连续内存空间⽽提前触发下⼀次收集。 不过,G1相对于CMS仍然不是占全⽅位、压倒性优势的,从它出现⼏年仍不能在所有应⽤场景中代替CMS就可以得知这个结论。⽐起CMS,G1的弱项也可以列举出不少,如在⽤户程序运⾏过程中,G1⽆论是为了垃圾收集产⽣的内存占⽤(Footprint)还是程序运⾏ 时的额外执⾏负载(Overload)都要⽐CMS要⾼。 ⽬前在⼩内存应⽤上CMS的表现⼤概率仍然要会优于G1,⽽在⼤内存应⽤上G1则⼤多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB⾄8GB之间。

垃圾收集器的权衡

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟 (Latency),三者共同构成了一个“不可能三角”。三者总体的表现会随技术进步而越来越好,但是要在这三个方面同时具有卓越表现的“完美”收集器是极其困难甚至是不可能的,一款优秀的收集器通常最多可以同时达成其中的两项。

我们应该如何选择一款适合自己应用的收集器呢?这个问题的答案主要受以下三个因素影响:

  1. ·**运行应用的基础设施如何?**譬如硬件规格,要涉及的系统架构是x86-32/64、SPARC还是 ARM/Aarch64;处理器的数量多少,分配内存的大小;选择的操作系统是Linux、Solaris还是Windows 等。
  2. **应用程序的主要关注点是什么?**如果是数据分析、科学计算类的任务,目标是能尽快算出结果, 那吞吐量就是主要关注点;如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务 超时,这样延迟就是主要关注点;而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是 不可忽视的。
  3. ·**使用JDK的发行商是什么?版本号是多少?**是ZingJDK/Zulu、OracleJDK、Open-JDK、OpenJ9抑 或是其他公司的发行版?该JDK对应了《Java虚拟机规范》的哪个版本?

内存分配与回收策略

对象优先在Eden分配:

⼤多数情况下,对象在新⽣代Eden区中分配。当Eden区没有⾜够空间进⾏分配时,虚拟机将发起⼀次Minor GC。

⼤对象直接进⼊⽼年代:

⼤对象就是指需要⼤量连续内存空间的Java对象,HotSpot虚拟机提供了-XX: PretenureSizeThreshold参数,指定⼤于该设置值的对象直接在⽼年代分配,这样做的⽬的就是避免在Eden区及两个Survivor区之间来回复制,产⽣⼤量的内存复制操作。

⻓期存活的对象进⼊⽼年代:

虚拟机给每个对象定义了⼀个对象年龄(Age)计数器,存储在对象头中(详⻅第2章)。对象通常在Eden区⾥诞⽣,如果经过第⼀次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中 每熬过⼀次Minor GC,年龄就增加1岁,当它的年龄增加到⼀定程度(默认为15),就会被晋升到⽼年代中。对象晋升⽼年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置。

动态对象年龄判断:

为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须 达到- XX: MaxTenuringThreshold才能晋升⽼年代,如果**在Survivor空间中相同年龄所有对象⼤⼩的总和⼤于Survivor空间的⼀半,年龄⼤于或等于该年龄的对象就可以直接进⼊⽼年代,**⽆须等到-XX: MaxTenuringThreshold中要求的年龄。

空间分配担保:

在发⽣Minor GC之前,虚拟机必须先检查⽼年代最⼤可⽤的连续空间是否⼤于新⽣代所有对象总空间,如果这个条件成⽴,那这⼀次Minor GC可以确保是安全的。如果不成⽴,则虚拟机会先查看-XX: HandlePromotionFailure参数的设置值是否允许担保失败 (Handle Promotion Failure); 如果允许,那会继续检查⽼年代最⼤可⽤的连续空间是否⼤于历次晋升到⽼年代对象的平均⼤⼩,如果⼤于,将尝试进⾏⼀次Minor GC,尽管这次 Minor GC是有⻛险的; 如果⼩于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进⾏⼀次Full GC。

新⽣代使⽤复制收集算法,但为了内存利⽤率, 只使⽤其中⼀个Survivor空间来作为轮 换备份,因此当出现⼤量对象在Minor GC后仍然存活的情况——最极端的情况就是内存回收后新⽣代中所有对象都存活,需要⽼年代进⾏分配担保,把Survivor⽆法容纳的对象直接送⼊⽼年代,这与⽣活中贷款担保类似。⽼年代要进⾏这样的担保,前提是⽼年代 本身还有容纳这些对象的剩余空间,但⼀共有多少对象会在这次回收中活下来在实际完成 内存回收之前是⽆法明确知道的,所以只能取之前每⼀次回收晋升到⽼年代对象容量的平 均⼤⼩作为经验值,与⽼年代的剩余空间进⾏⽐较,决定是否进⾏Full GC来让⽼年代腾 出更多空间。

JDK 6 Update 24之后的规则变为只要⽼年代的连续空间⼤于新⽣代对象总⼤⼩或者历次晋升的平均⼤⼩,就会进⾏Minor GC,否则将进⾏Full GC。(不再使⽤ -XX: HandlePromotionFailure参数了)
空间进⾏⽐较,决定是否进⾏Full GC来让⽼年代腾 出更多空间。

JDK 6 Update 24之后的规则变为只要⽼年代的连续空间⼤于新⽣代对象总⼤⼩或者历次晋升的平均⼤⼩,就会进⾏Minor GC,否则将进⾏Full GC。(不再使⽤ -XX: HandlePromotionFailure参数了)


版权声明:本文为Cavneko原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。