JVM之垃圾回收器与内存分配策略
1. 对象是否存活
垃圾回收器在对堆进行回收钱,第一件事情就是要确定对象是否存活
1.1 引用计数法
算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。但JAVA没有选用引用计数算法来管理内存,主要的原因是很难解决对象之间的相互循环引用的问题
1.2 根搜索算法
算法:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots 没有任何引用链相连(即GC Roots到这个对象不可达),则证明此对象是不可用的
在java中,可作为GC Roots的对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象;方法区中的类静态属性引用的对象;方法区中的常量引用的对象;本地方法栈中的JNI(nateive方法)的引用的对象
1.3 再谈引用
JDK1.2后引用分为强引用、软引用、弱引用、虚引用
- 强引用,类似
Object obj = new Object()
这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象 - 软引用(soft reference),描述一些还有用,但非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中并进行第二次回收
- 弱引用(weak reference),也是用来描述非必须对象,但是它的强度比软引用更弱一下,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
- 虚引用,最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知
即使算法已经判定这个对象“非死不可”,但是他们也只是出于死缓阶段,要真正判定他死亡,至少要经历两次标记过程;如果对象在第一个进行可达性分析后发现没有与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能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式取处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
G1收集器的运作大致可划分为以下几个步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
文章图片
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 收集器时生效 |
- 对象优先分配在Eden区
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC
注意:
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕灭的特性,所以Minor GC 非常频繁
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对,在 ParallelScavenge收集器的收集策略里,就有直接进行Major GC的策略选择过程),Major GC的速度一般会比Minor GC慢10倍以上
- 大对象直接进入老年代
所谓大对象,是指需要大量连续的内存空间的JAVA对象,比如很长的字符串或数组。虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个大小的对象直接进入老年代。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝
注意:
PretenureSizeThreshold 参数只对Serial和ParNew两个收集器生效,ParallelScavenge 不认识该参数,ParallelScavenge 一般不需要设置,如果遇到必须使用此参数的场合,可以考虑使用ParNew和CMS的收集器组合
- 长期存活的对象将进入老年代
虚拟机采用了分代收集的思想,为了做到这点,虚拟机给每个对象定义了对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
- 动态对象年龄判定
为了能更好的的适应不同程序的内存状况,虚拟机并不是永远得要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
- 空间分配担保
在发生Minor GC时,虚拟机就会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,则改为直接进行一次Full GC.如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC:如果不允许,则也要改为进行一次Full GC.
新生代使用复制收集算法,但为了内存的利用率,只使用其中一个Survivor空间作为轮换备份,因此当出现大量对象在Minor GC 后仍然存活的情况,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。如果老年代剩余空间不足就需要进行Full GC。
【JVM之垃圾回收器与内存分配策略】取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。 如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GG。虽然担保失败时绕的圈子是最大的,但大部分情况下还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁
推荐阅读
- PMSJ寻平面设计师之现代(Hyundai)
- 太平之莲
- 闲杂“细雨”
- 七年之痒之后
- 深入理解Go之generate
- 由浅入深理解AOP
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 生活随笔|好天气下的意外之喜
- 感恩之旅第75天
- python学习之|python学习之 实现QQ自动发送消息