本篇主要讲 垃圾收集算法 、 HotSpot的的算法实现 和 垃圾收集器。
垃圾收集算法
标记-清除算法
Mark-Sweep,最基础的算法。
image.png
过程:(2阶段)
阶段一:标记要回收的对象;
阶段二:清除被标记的对象。
优点:
- 内存使用高
缺点:
- 效率慢:标记和清除两个过程的效率都不高;
- 空间浪费:清除完后产生大量不连续的内存碎片,导致下次分配大对象时不得不再触发一次GC。
复制算法
Copying,将内存分为均等的两块,每次只使用一块。解决了标记-清除的效率问题。
image.png
图中保留区域是另一半未使用的内存。
过程:(2阶段)
(前置:将内存均等为两块:内存块A 和 内存块B)
阶段一:复制存活的对象从内存块A到内存块B;
阶段二:清除内存块A。
优点:
- 效率高(如果复制时存活量大的话,该部分同样是瓶颈);
缺点:
- 内存使用缩小了。
标记-整理算法
Mark-Compact,在标记-清除算法上添加整理功能。
image.png
过程:(3阶段)
阶段一:标记要回收的对象;
阶段二:清除被标记的对象;
阶段三:整理存活对象至一端,清理端边界意外的内存。
优点:
- 内存使用高
缺点:
- 效率慢:标记和清除两个过程的效率都不高;
分代收集算法
根据内存中对象的生存周期来按块回收。
在Java中,堆分为新生代
和老年代
。
在新生代中每次都有大量的对象死去,只有少量存活,采用 复制算法。
在老年代中对象的存活率高,没有额外空间进行分配担保,就必须使用标记-清理 或标记-整理算法进行回收。
在HotSpot中,新生代分为1个Eden空间、2个Survivor空间,默认Eden和Survivor大小比为8:1。每次使用Eden和其中一个Survivor,当回收时,将Eden和该Survivor中存活的对象一次性复制到另一块Survivor空间,然后清理Eden和刚才用过的Survivor。当Survivor空间不够时,老年代参与分配担保机制。
HotSpot的的算法实现
HotSpot的算法实现,依赖于以下几点技术:
- 枚举根节点:
在GC时,必须要在某一确保一致性的快照中进行,即要保证分析结果的准确性。也称Stop The World。由于主流JVM都是使用准确式GC,所以JVM知道哪些东西存放在哪里。在HotSpot的实现中是使用OopMap的数据结构来达到这个目的的。
OopMap扩展
Java底层是以字节指令来执行的,但又不能每条指令都生成OopMap,太耗空间。在HotSpot的实现中只有在安全点时才会生成OopMap。
OopMap数据结构:OopMap{ebx=Oop [16]=Oop off=142}
指明了EBX寄存器和栈中偏移量为16的内存区域各有一个普通对象指针(Ordinary Object Pointer)的引用。
off是OopMap记录的偏移量,在本地码中,mov指令+off=hlt指令。
- 安全点(SafePoint):让程序长时间执行的特征的位置。
“长时间执行”最明显的特征就是指令复用,如方法调用、循环跳转、异常跳转等。所以只有具备这些功能的指令才会可能产生安全点。
让现场停到安全点,有两种方案:抢先式中断 和 主动式中断。HotSpot采用主动式中断。
抢先式中断,在GC时,让所有线程都中断,如果有没有中断到安全点上的,恢复并使其“跑”到安全点上。现在几乎没有虚拟机采用这种方式。
主动式中断,在GC时,设置一标志,各个线程轮询这个标志,发现中断标志为真则中断自己。轮询标志和安全点是重合的。
- 安全区域(SafeRegion):在一段代码片段中,引用关系不会发生变化。可以理解为扩展版安全点。
当线程sleep或blocked时,此时线程无法响应JVM的中断请求,JVM显然也不会等待线程重新分配CPU时间。此时会在安全区域中进行GC。
在线程执行到Safe Region中的代码时,先标记自己已经进入到了Safe Region,那么JVM若在这段时间内GC,是不用管Safe Region状态的线程的。在该线程要离开Safe Region时,它会检查系统是否已经完成了根节点枚举(或者整个GC),若完成了,线程继续执行;否则等待其完成。
垃圾收集器
这段讲述Serial收集器
、ParNew收集器
、Parallel Scavenge收集器
、Serial Old收集器
、Parallel Old收集器
、CMS收集器
、G1收集器
的说明、使用场景。
Serial | ParNew | Parallel Scavenge | Serial Old | Parallel Old | CMS | G1 | |
---|---|---|---|---|---|---|---|
单/多线程 | 单 | 多 | 多 | 单 | 多 | 多 | 多 |
并行/并发 | - | 并行 | 并行 | - | 并行 | 并发 | 并发 |
Client/Server | Client | Server | Server | 主C副S | Server | Server | Server |
所属年代 | 新生代 | 新生代 | 新生代 | 老年代 | 老年代 | 老年代 | 所有 |
算法 | - | - | 复制 | - | 复制 | 标记-清除 | 整体标记-整理,局部复制 |
上面的表格是各个收集器的一些通用属性的比较,下面是对各个收集器的一些补充,包括其主要配置参数。
所有的单线程收集器都是串行的。
所有的新生代都是采用复制算法。
Serial收集器
这是一个最基本、历史悠久的收集器。他会暂停其他所有工作线程,来执行它的垃圾收集,直到收集结束才会恢复其他线程。所以改收集器的别名:串行收集器。
Serial收集器由于没有线程交互开销,可以专心的垃圾回收,自然其可以获得最高的单线程收集效益。而在用户桌面应用场景中,分配给JVM的内存不会很大。Serial收集器收集两百兆以内的新生代,Stop The World(以下简称STW)停顿时间可以控制在几十毫秒左右,而这也是客户可以接受的。
ParNew收集器
相关参数:
-XX:ParallelGCThreads
:限制垃圾收集的线程数。
ParNew收集器在单线程环境中绝对不比Serial收集器工作得好。由于其存在线程交互的开销,该收集器通过超线程技术实现的两个CPU的环境中都不能100%保证超过Serial收集器。但其对资源使用率有效充分。
Tips:超线程技术,CPU可以同时执行多个线程的一种技术,提升了资源使用率,但CPU的性能很难有突破。
Parallel Scavenge收集器
这是一个以达到一个可控制的吞吐量为目的的收集器。也叫吞吐量优先收集器。
相关参数:
-XX:MaxGCPauseMills
:设置最大垃圾的停顿时间(毫秒数)。x>0。
-XX:GCTimeRatio
:设置GC时间比率。100>x>0。该值的意义是最多花费(1/(1+x))来收集最大1%的垃圾。如x=19,即最多花费5%的时间来收集1%的垃圾。其中1%是默认值。默认的垃圾值为99,1/(1+99)=1%。
-XX:+UseAdaptiveSizePolicy
:自动调配新生代参数(如大小、Eden:Survivor比例、晋升老年代年龄等)。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),在吞吐量中,垃圾收集时间就是停顿时间,而GC的停顿时间太短,会导致GC的频繁,这样吞吐量也就降低了。
该收集器追求的高吞吐量,适用于后台运算(运算与CPU有关,这样可以充分利用CPU)而不需要太多交互的任务。
Serial Old收集器
Serial的老年代版本。
使用场景
主要给Client模式下的虚拟机使用。
在Server端,一是给JDK1.5及之前的Parallel Scavenge收集器搭配;一是CMS收集器的后备方案(见CMS)。
Parallel Old收集器
Parallel Scavenge的老年代版本。
CMS收集器
Concurrent Mark Sweep,并发标记清除收集器。一种以获取最短回收停顿时间为目标的收集器。from JDK1.5。
过程:
- 初始标记:需要STW。只标记和GC Roots可以直接关联的对象。速度快。
- 并发标记:需要STW。GC Roots Tracing过程。可以与用户线程并发。
- 重新标记:修正并发期间因用户程序继续运行而导致标记变动的部分。时间大于初始标记时间,小于并发标记时间。
- 标记清除:清除标记对象。可以与用户线程并发。
优点:
- 并发收集;
- 低停顿。
缺点:
- CPU资源敏感;
CMS默认的回收垃圾线程数为(CPU数量+3)/4
,当CPU数>4,线程占不少于25%的CPU资源;当CPU<4时,线程的CPU资源占有率变大。 - 无法处理浮动垃圾;
浮动垃圾是CMS并发清理阶段,程序运行产生的新垃圾对象。也就是说需要预留足够的空间给并发的用户线程使用。在JDK 1.5时,CMS默认的老年代激活阈值为68%,JDK 1.6时,该值为92%。相关参数:-XX:CMSInitiatingOccupancyFraction
。 - 空间碎片。
空间碎片是“标记-清除”算法的缺点。CMS提供了两个参数:-XX:+UseCMSCompactAtFullCollection
和-XX:CMSFullGCsBeforeCompaction
。
相关参数
-XX:CMSInitiatingOccupancyFraction
:CMS初始化容量百分比。
若CMS运行期间预留内存不够程序所需,会出现“Concurrent Mode Failure”失败,此时JVM会启动备案:临时启动Serial Old收集器来收集,而此时的停顿时间就更长了。
-XX:+UseCMSCompactAtFullCollection
:在CMS进行Full GC时进行碎片整合。默认开启。内存整理过程无法并发,所以时间变长。
-XX:CMSFullGCsBeforeCompaction
:执行n次不压缩的Full GC后,跟着来一次带压缩的。(默认为0,即每次Full GC都会整理)
G1收集器
Garbage-First收集器。
优点:
- 并行与并发;
充分利用多CPU,多核环境,使用多CPU(核)来缩短STW时间。 - 分代收集;
采用不同的方式去处理新创建的对象和已经存活一段时间的对象。 - 空间整合;
从整体看,基于“标记-整理”算法,从局部看是基于“复制”算法实现。而这两种算法都不会产生空间碎片。 - 可预测的停顿。
G1比CMS的优势所在。G1能建立可预测的停顿时间模型,让使用者明确指定一个长为M毫秒的时间片段内,GC时间不得超过N毫秒。
G1的堆结构
使用G1时,Java堆结构有别于其他收集器。它将整个Java堆分为多个大小相等独立的Region,虽然还有新生代、老年代的概念,但他们不再物理隔离,都是一部分Region(不需要连续)的集合。
G1的停顿时间模型
G1之所以可以建立停顿时间模型,因为G1可以有计划的避免全区垃圾扫描。G1跟踪各个Region里的垃圾堆价值大小(回收获得的空间大小以及回收所需时间的经验值),在后台维护一个列表,每次根据允许的收集时间,优先回收价值最大的Region(这是Garbage-First名字的由来)。这种按区划分空间以及按优先级回收区域的方式,保证了G1在优先的时间内可以获得更高的收集效率。
G1的化整为零
G1的内存思路是“化整为零”。G1中的所有Region都不是孤立的。在G1中所有的Region都有一个Remembered Set与之关联,在对Reference类型数据进行写操作时,会暂停中断并检查Reference引用的对象所属Region,并写到自己Region的Remembered Set中。当进行回收时,在GC Roots的枚举范围内加入Remembered Set来避免全堆扫描。
过程:(不含维护Remembered Set)
- 初始标记:只标记和GC Roots可以直接关联的对象,并修改TAMS的值。需要停顿线程,耗时短。
- 并发标记:从GC Roots对堆进行可达性分析,找出存活对象。耗时长。可与用户程序并发执行。
- 最终标记:修正并发标记期间因用户程序继续运行而导致标记变动的那部分标记记录。将这段时间的变化记录在Remembered Set Logs中,最终将其合并到Remembered Set中。需要停顿,但可并行。
- 筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望来GC。只回收一部分Region。并行。
设置:通过配置-XX:+UseG1GC
参数来使用。
当应用中存活的对象数量突然发生变化的时候,会对gc的暂停时间有很大影响。我们可以通过-XX:G1MaxNewSizePercent
参数降低最大年轻代的大小,从而减少暂停期间最大需要回收的年轻代对象。
联想:G1的最终标记中Logs到Set的做法,类似的还有ES的trancingLog到segment,Redis的aof到内存。
GC日志查看
//TODO:以后查看日志了,复制一段,然后解说
网友评论