JVM之垃圾回收器与内存分配策略

2020-02-27  本文已影响0人  VayneP

1. 对象是否存活

垃圾回收器在对堆进行回收钱,第一件事情就是要确定对象是否存活

1.1 引用计数法

算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。但JAVA没有选用引用计数算法来管理内存,主要的原因是很难解决对象之间的相互循环引用的问题

1.2 根搜索算法

算法:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots 没有任何引用链相连(即GC Roots到这个对象不可达),则证明此对象是不可用的

在java中,可作为GC Roots的对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象;方法区中的类静态属性引用的对象;方法区中的常量引用的对象;本地方法栈中的JNI(nateive方法)的引用的对象

1.3 再谈引用

JDK1.2后引用分为强引用、软引用、弱引用、虚引用

1.4 生存还是死亡

即使算法已经判定这个对象“非死不可”,但是他们也只是出于死缓阶段,要真正判定他死亡,至少要经历两次标记过程;如果对象在第一个进行可达性分析后发现没有与GC ROOTS相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。如果这个对象没有覆盖finalize()方法,或者finalize已经被调用过,那么这个对象才会被正在的执行死刑。

首先第一次这个对象被判定为有必要执行finalize()方法,那么这个对象会被放置在一个叫做F-Queue的队列之后,之后会被一个低优先级的小线程执行。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将会对F-queue重的对象进行第二次标记,如果对象要在finalize()中拯救自己需要重新与引用链上的任何一个对象建立关联,如果在第二次标记前,这个对象还没有移除即将回收的集合,那么它基本上就要被回收了。

2. 垃圾收集算法

2.1 标记-清除算法(Mark-Sweep)

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有标记的对象

img

缺点:一个是效率问题,标记和清除过程的效率都不高;另一个是空间问题,内存碎片化严重,后续可能发生大对象不能找到可利用空间

2.2 复制算法(Copying)

为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉

img

缺点:内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

2.3 标记 - 整理算法(Mark-Compact)

标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象

img

2.4 分代收集算法(Generational Collection)

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)

3. 垃圾回收器

[图片上传失败...(image-8713c1-1583046941026)]

3.1 Serial收集器------复制算法

特点

针对新生代;采用复制算法;单线程收集;进行垃圾收集时,必须暂停所有工作线程,直到完成;

应用场景

依然是HotSpot在Client模式下默认的新生代收集器;

也有优于其他收集器的地方:简单高效(与其他收集器的单线程相比);

对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;在用

户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一

百多MS),只要不频繁发生,这是可以接受的

3.2 ParNew收集器------复制算法

特点

除了多线程外,其余的行为、特点和Serial收集器一样;

如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等

应用场景

在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工

作(因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现,而其余几种收集

器则共用了部分的框架代码); 但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销

3.3 Parallel Scavenge收集器

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector),通过设置参数进行控制吞吐量。参数分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数和直接设置吞吐量大小的-XX:GCTimeRatio参数

MaxGCPauseMillis参数是大于0的毫秒数。收集器保证内存回收耗费的时间不超过设定值,不过GC停顿个时间是以牺牲吞吐量和新生代空间来换取的。

GCTimeRatio参数的值是大于0小于100的整数,也就是垃圾时间站总时间的比率,比如参数设为19,最大GC时间战总时间的5%(1/(1+19))。默认值是99,就是允许最大时间的1%作为垃圾收集时间

还有一个参数-XX:+UseAdaptiveSizePolicy,这是开关参数,打开以后不需要指定新生代大小(-Xnn)、Eden与Survivor区的比例 (-XX:SurvivorRatio) 、晋升到老年代的年龄(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据实际情况动态调节这些参数以提供最合理的停顿时间以及最大的吞吐量。

特点

新生代收集器; 采用复制算法;多线程收集;

主要特点是:它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿

时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);有自适应调节策略;

应用场景

高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;当应用程序运行在具有多个CPU上,对

暂停时间没有特别高的要求 时,即程序主要在后台进行计算,而不需要与用户进行太多交互;例如,那些执行批

量处理、订单处理、工资支付、科学计算的应用程序;

3.4 Serial Old收集器

Serial Old是 Serial收集器的老年代版本

特点

针对老年代;采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);单线程收集;

应用场景

主要用于Client模式;

而在Server模式有两大用途:(A)、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);(B)、作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解)

3.5 Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;JDK1.6中才开始提供;

特点

针对老年代; 采用"标记-整理"算法; 多线程收集;

应用场景

JDK1.6及之后用来代替老年代的Serial Old收集器;

特别是在Server模式,多CPU的情况下;

这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合

3.6 CMS收集器

CMS(Concurrent Mark Sweep,CMS)收集器是一种以获取最短回收停顿时间为目标的收集器,也称为并发低

停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器

特点

针对老年代;基于"标记-清除"算法(不进行压缩操作,产生内存碎片);

以获取最短回收停顿时间为目标;

并发收集、低停顿; 需要更多的内存(看后面的缺点);

是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;

第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

应用场景

与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度;以给用户带来较好的体验; 如常见WEB、B/S系统的服务器上的应用;

运作过程

(A)、初始标记(CMS initial mark)----- 仅标记一下GC Roots能直接关联到的对象; 速度很快; 但需要"Stop The World";

(B)、并发标记(CMS concurrent mark)------ 进行GC Roots Tracing的过程;刚才产生的集合中标记出存活对象;应用程序也在运行;并不能保证可以标记出所有的存活对象;

(C)、重新标记(CMS remark)------为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率;

(D)、并发清除(CMS concurrent sweep)-----回收所有的垃圾对象;整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作; 所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;

img

缺点

(A)CMS收集器对CPU资源非常敏感。并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低;CMS的默认收集线程数量是=(CPU数量+3)/4,当CPU数量多于4个,收集线程占用的CPU资源最多不超过25%,但不足4个时,影响就很大

(B)CMS收集器无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败导致另一次“Full GC”的产生。在并发清除时,用户线程新产生的垃圾,称为浮动垃圾; 由于垃圾收集阶段用户线程还需要运行,这使得并发清除时需要预留一定的内存空间(默认情况下,老年代空间使用了68%的空间后会被激活,若老年代增长不是太快,可以调高参数 -XX:CMSInititatingOccupancyFraction来提高触发百分比),不能 像其他收集器在老年代几乎填 满再进行收 集;如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集 器

(C)垃圾收集结束后产生大量空间碎片,往往会出现老年代还有很大的空间剩余,但无法找到足够大的连续空间分配当前对象,而不得不触发Full GC,所以CMS收集器提供了 -XX:+UseCMSCompactAtFullCollection开关参数,用于在执行完Full GC后提供一个碎片整理过程。

3.7 G1收集器

G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器很很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,它们都是一部分Region(不需要连续)的集合。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1收集器的运作大致可划分为以下几个步骤:

G1收集器的运作步骤中并发和需要停顿的阶段:

img

3.8 垃圾收集器常用参数

UseSerialGC 虚拟机运行在Client 模式下的默认值,打开此开关后,使用Serial +Serial Old 的收集器组合进行内存回收
UseParNewGC 打开此开关后,使用ParNew + Serial Old 的收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为CMS 收集器出现Concurrent Mode Failure失败后的后备收集器使用
UseParallelGC 虚拟机运行在Server 模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收
UseParallelOldGC 打开此开关后,使用Parallel Scavenge + Parallel Old 的收集器组合进行内存回收,新生代中Eden 区域与Survivor 区域的容量比值, 默认为8, 代表Eden :Survivor=8∶1
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC 之后,年龄就加1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy 动态调整Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden 和Survivor 区的所有对象都存活的极端情况
ParallelGCThreads 设置并行GC 时进行内存回收的线程数
GCTimeRatio GC 时间占总时间的比率,默认值为99,即允许1% 的GC 时间。仅在使用Parallel Scavenge 收集器时生效
MaxGCPauseMillis 设置GC 的最大停顿时间。仅在使用Parallel Scavenge 收集器时生效
CMSInitiatingOccupancyFraction 设置CMS 收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS 收集器时生效
UseCMSCompactAtFullCollection 设置CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS 收集器时生效
CMSFullGCsBeforeCompaction 设置CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用CMS 收集器时生效

4. 内存分配与回收策略

注意:

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕灭的特性,所以Minor GC 非常频繁

老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对,在 ParallelScavenge收集器的收集策略里,就有直接进行Major GC的策略选择过程),Major GC的速度一般会比Minor GC慢10倍以上

注意:

PretenureSizeThreshold 参数只对Serial和ParNew两个收集器生效,ParallelScavenge 不认识该参数,ParallelScavenge 一般不需要设置,如果遇到必须使用此参数的场合,可以考虑使用ParNew和CMS的收集器组合

上一篇 下一篇

猜你喜欢

热点阅读