二、java虚拟机夯实基础--垃圾收集器(面向堆)与内存分配策略(下)

4 具体的经典垃圾收集器

收集算法是内存回收的方法论,垃圾收集器就是内存回收的实践者。

4.1 Serial收集器

Serial收集器是最基础、历史最悠久的收集器,这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”

4.2 ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为都与Serial收集器完全一致。

JDK 7之前的遗留系统中⾸选的新⽣代收集器,其中有⼀个与功能、性能⽆关但其实很重要的原因是: 除了Serial收集器外,⽬前只有它能与CMS收集器配合⼯作。

在JDK 5中使⽤CMS来收集⽼年代的时候,新⽣代只能选择ParNew或者Serial收集器中的⼀个。

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

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

4.3 Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器。

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

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

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

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

4.4 Serial Old收集器

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

4.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器对应的常用老年代gc收集器是serial old,但是是多线程(Parallel Scavenge)匹配(Serial old,所以系统的吞吐量不一定高)。

直到Serial old出现:注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合

4.6 CMS收集器(并发低停顿收集器)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。适合:互联网网站等要求响应时间短的需求。

CMS收集器是基于标记-清除算法实现的,包括:

1、初始标记:标记 GC Roots 直接关联的对象,会导致 STW ,但是这个没多少对象,时间短 。

2、并发标记:从 GC Roots 开始关联的所有对象开始遍历整个可达路径的对象,这步耗时比较长,所以它允许用户线程和GC线程并发执行,并不会导致STW ,但面临的问题是可能会漏标,多标,等问题。

3、重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段会导致 STW ,但是停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

4、并发清除;将被标记的对象清除掉,因为是标记-清除算法,不需要移动存活对象,所以这一步与用户线程并发运行。

5、重置线程:重置GC线程状态,等待下次CMS的触发,与用户线程同时运行。

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

由于在整个过程中耗时最⻓的并发标记和并发清除阶段中,垃圾收集器线程都可以与⽤户,线程⼀起⼯作,所以从总体上来说,CMS收集器的内存回收过程是与⽤户线程⼀起并发执⾏的。

CMS的三个缺点:

(1)CMS收集器对处理器资源⾮常敏感对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大,因为CMS默认启动的回收线程数量是:(CPU核数+3)/ 4。

(2)CMS收集器⽆法处理“浮动垃圾”(FloatingGarbage)

由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉,这一部分垃圾就称为“浮动垃圾”(比如用户线程运行产生了新的 GC Roots )。

由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收,那就还需要预留⾜够内存空间提供给⽤户线程使⽤,

要是CMS运⾏期间预留的内存⽆法满⾜程序分配新对象的需要,就会出现⼀次“并发失

败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案: 冻结⽤户线程的执⾏,临时启⽤Serial Old收集器来重新进⾏⽼年代的垃圾收集但这样停顿时间就很⻓

了。所以参数-XX: CMSInitiatingOccupancyFraction设置得太⾼将会很容易导致⼤量的并发失败产⽣,性能反⽽降低,⽤户应在⽣产环境中根据实际应⽤情况来权衡设置。

(3)⼤量空间碎⽚:

CMS是⼀款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有⼤量空间碎⽚产⽣。空间碎⽚过多时,将会给⼤对象分配带来很⼤麻烦,往往会出现⽼年代还有很多剩余空间,但就是⽆法找到⾜够⼤的连续空间来分配当前对象,⽽不得

不提前触发⼀次Full GC的情况

因此虚拟机设计者们还提供了另外⼀个参数-XX: CMSFullGCsBefore- Compaction(此参数从JDK 9开始废弃),这个参数的作⽤是要求CMS收集器在执⾏过若⼲次(数量由参数值决定)不整理空间的Full GC之后,下⼀次进⼊Full GC前会先进⾏碎⽚整理(默认值为0,表示每次进⼊Full GC时都进⾏碎⽚整理)。

4.7 Garbage First收集器(G1)

G1是一款主要面向服务端应用的垃圾收集器。JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。G1开创了收集器面向局部收集的设计思路以及基于 Region 的内存布局形式。

G1的产生是为解决CMS算法产生空间碎片和其它一系列的问题缺陷,oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。

设计思想:

  1. 思想转变:要实现这个目标,首先要有一个思想上的转变,G1收集器出现之前的其他所有收集器,他们的收集范围要么是新生代( Minor GC ),要么是老年代( Major GC ),要么是整堆( Full GC ),而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪个回收集中存放的垃圾最多,回收收益最大,就回收哪个,这就是G1收集器的Mixed GC模式。

  2. 新的内存布局:G1能达到这个目标的关键在于G1开创了基于Region 的堆内存布局当然也依然遵循了分代收集理论,但是堆内存布局与其他收集器有明显差异,G1不在坚持固定大小以及固定数量的分代区域划分,而是把连续的java堆内存划分成多个大小相等独立区域( Region ),每个Region 可以根据需要扮演新生代的 Eden , Survivor ,或者老年代。收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是对于新创建的对象还是对于熬过很多次垃圾收集的旧对象都有很好的收集效果。

Region 的大小可以通过参数 -XX:G1HeapRegionSize=value 设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。

Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象G1认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象,对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的Humongous Region 之中。G1仍然保留了新生代,老年代的概念,只不过它们不再连续和固定的了。


3.回收策略:G1之所以能建立可预测的“停顿时间模型”的原因在于它将Region 作为单次回收的最小单元,即每次回收的空间都是 Region 的整数倍,同时G1会去追踪各个 Region 里面垃圾的“价值”(回收所获得的空间大小以及回收所需要的时间的经验值),然后在后台维护一个优先级列表,每次根据用户设定停顿时间( -XX:MaxGCPauseMillis=time ,默认200毫秒),优先回收价值收益最大的那些 Region 。

回收时G1 将存活的对象从堆的一个或多个 Region 复制到堆上的单个其他Region ,并在此过程中压缩和释放内存。这个工作是在多处理器上并行执行,以减少暂停时间并提高吞吐量。因此,每次垃圾回收时,G1 都会不断努力减少碎片

G1将堆内存“化整为零”的思路看起来不难理解,但是有很多细节问题需要解决:

1、跨 Region 引用如何解决:前面我们知道通过记忆集( RSet )解决跨代引用,但是在G1中,每个 Region 都需要维护自己的记忆集,记录别的 Region指向自己,但是G1中的 Region 数量要比传统收集器的分代数量明显多的多,所以G1中使用记忆集要比其他收集器有着更高的内存占用负担,根据经验,G1至少要耗费大约相当于java堆容量的10%~20%。

2、并发标记问题:如何保证并发标记阶段GC收集线程与用户线程互不干

扰,当然G1是通过原始快照( SATB )解决的(CMS是通过增量更新实现的)。

另外一个需要解决的就是阶段如何处理用户线程新创建对象的内存分配,G1的做法是为每个 Region 设计了两个名为 TAMS ( Top at Mark Start )的指针,把 Region 中的一部分空间划分出来用于存放并发回收过程中的新对象分配,新分配的对象地址都必须要在这两个指针位置以上,G1收集器在本次回收时默认这些对象是存活的,不回收的。与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结⽤户线程执⾏,导致Full GC⽽产⽣⻓时间“Stop The

World

3、如何建立可靠的可预测模型:用户通过 -XX:MaxGCPauseMillis=time

参数指定的停顿时间只是一个期望值,但是G1怎么做才能满足用户的期望呢?G1收集器在收集过程中会记录每个 Region 的回收耗时,每个 Region 记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析出平均值,标准偏差,置信度等统计信息。根据这些信息决定 Region 的回收价值。

运行过程

1、初始标记:标记出 GC Roots 直接关联的对象,并且修改TAMS指针的值,这个阶段速度较快,STW,单线程执行,

2、并发标记:从 GC Root 开始对堆中的对象进行可达性分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。

3、重新标记:修正在并发标记阶段因用户程序执行而产生变动的标记记录,即处理 SATB 记录。STW,并发执行。

4、回收筛选(是多个回收线程并行,但是,不是并发(并发指用户线程和回收线程同时运行)):筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,筛出后移动合并存活对象到空Region,清除旧的,完工。因为这个阶段需要移动对象内存地址,所以必须STW。

总结:

G1前面的几步和CMS差不多,只有在最后一步,CMS是标记清除G1需要合并Region属于标记整理。

优缺点

1、并发性:继承了CMS的优点,可以与用户线程并发执行。当然只是在并发标记阶段。其他还是需要STW

2、分代GC:G1依然是一个分代回收器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。而其他回收器,或者工作在年轻代,或者工作在老年代;

3、空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提升内部循环速度。

4、可预测性:为了缩短停顿时间,G1建立可预存停顿的模型,这样在用户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。

建议:

1、如果应用程序追求低停顿,可以尝试选择G1;

2、经验值上,小内存6G以内,CMS优于G1,超过8G,尽量选择G1

3、是否代替CMS只有需要实际场景测试才知道。(如果使用G1后发现性

能还不如CMS,那么还是选择CMS)

5.选择垃圾收集器的权衡

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

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

1应⽤程序的主要关注点是什么? 如果是数据分析、科学计算类的任务,⽬标是能尽快算出结果,那吞吐量就是主要关注点; 如果是SLA应⽤,那停顿时间直接影响服务质量,严重的甚⾄会导致事务超时,这样延迟就是主要关注点; ⽽如果是客户端应⽤或者嵌⼊式应

⽤,那垃圾收集的内存占⽤则是不可忽视的。

2.运⾏应⽤的基础设施如何? 譬如硬件规格,要涉及的系统架构是x86-32/64、SPARC还是ARM /Aarch64; 处理器的数量多少,分配内存的⼤⼩; 选择的操作系统是Linux、Solaris还是Windows等。

3.使⽤JDK的发⾏商是什么? 版本号是多少? 是ZingJDK/Zulu、OracleJDK、Open-JDK、OpenJ9或是其他公司的发⾏版?该JDK对应了《Java虚拟机规范》的哪个版本? 一般来说,收集器的选择就从以上这⼏点出发来考虑。举个例⼦,假设某个直接⾯向⽤户提供服务的B/S系统准备选择垃圾收集器,⼀般来说延迟时间是这类应⽤的主要关注点。

6.内存分配与回收策略

对象优先在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参数了)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值