GC 垃圾回收器
Posted 阿哲是哲学的哲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GC 垃圾回收器相关的知识,希望对你有一定的参考价值。
GC 垃圾回收器
Q : 为何需要垃圾回收?
当成功的识别存活的对象和需要回收的对象之后, 此时就需要由垃圾回收器释放掉需要被回收的对象, 让出新空间给新的对象
一. 垃圾回收算法
不同垃圾回收器, 有同的垃圾回收算法, 这样就决定了垃圾回收器的相关特性
1. 标记-清除算法
标记-清除算法是一种最原始的常见的算法
-
步骤
-
将需要存活的对象标记起来
-
回收没被标记的对象 标记: GC从根节点开始遍历所有被根节点应用的对象, 一般在Header中标记 清除: GC对堆内存从头到尾进行遍历对象, 如果对象没被标记到(header中没有标记为根可达 则回收该对象)
-
缺点:
-
效率低, 标记和清除都需要遍历对象 -
GC时需要Stop-the-World -
没有进行整理, 存储空间不连续 ,会产生空间碎片
2. 复制算法
将堆空间一分二, 空出一半的完整空间, 每次GC时 从A空间->B空间 移动存活的对象, 这样每次GC完成后都有连续的存储空间. 研究发现,新⽣代中的对象每次回 收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率还不错。不适合存活对象⽐较多的场景, 因为实际上削减了一般的使用内存
-
缺点: 削减了一般的使用内存, 每次GC时都会发送移动, 假设极端情况下所有对象都存活, 则白白移动多一次, 所以在存活率太高的场景下不适合
-
优点: 减少了标记这一步, 高效 , 能保证空间连续性, 没有空间碎片
3. 标记-整理算法
和标记清除类似, 只是这里的标记玩之后不是删掉无用的, 而是汇聚存活的对象到空间前面 标记: GC从根节点开始遍历所有被根节点应用的对象, 一般在Header中标记 整理: 将标记为存活的对象, 汇聚到对象空间的前面
-
缺点: 从效率上来说, 复制对象比删对象更低, 所以比标记-清除低一点移动对象完成后, 还需要改变所有对象的引用移动过程中也要stw -
优点 消除了标记-清除算法当中,不会产生内存碎片,我们需要给新对象分配内存时,JVM只需要持有⼀个内存的起始地址即可。能充分利用内存资源, 无需被砍2倍
标记-清除 | 标记-压缩 | 复制算法 | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎⽚) | 少(不堆积碎⽚) | 通常需要活对象的2倍⼤⼩(不堆积碎⽚) |
移动对象 | 否 | 是 | 是 |
4. 分代收集算法
对比上面的三种算法, 应该是各有优缺点的, java把堆分成新⽣代和⽼年代,这样就可以根据各个年代的特点使⽤不同的回收算法,以提⾼垃圾回收的效率。
4.1 如何分配?
-
新new出来的对象先会尝试存入Eden区
-
假设Eden区存满了, 此时就要开始 Minor GC(Young GC) , 清理Eden区不被别人引用的对象, 再尝试存入新的对象 Minor GC(Young GC) : 将Eden区中存活的对象 移动到 Survivor to区 将 Survivor from 区的对象, 移动到 Survivor to区, 并且标记年纪+1, 若年纪超过15 (可以设置参数: -XX:MaxTenuring
-
Survivor to 和 Survivor from 区是两个交替使用的区域, 一般也叫Survivor 0 、 Survivor 1 区
-
当老年区也满了, 则会触发 (Major GC / Full GC), 如果依旧装不下新对象, 则会出现OOM异常 (Major GC / Full GC): Major GC通常是跟full GC是等价的,收集整个GC堆。
-
新生代的垃圾回收最频繁. 老年代很少垃圾回收. 永久代几乎不回收 3.4.2(图片显示内存使用情况)
-
增量式垃圾回收 主要是利用在线程的充分利用, 允许垃圾回收分阶段进行, 而不是一下子全部进行完毕, 减少了STW的时间, 但是也增加了CPU上下文切换的成本, 导致吞吐量下降
-
分区算法 将Eden S1 S2 old 各自拆分出多个独立的小区间, 这样就可以独立回收一部分的小区间即可, 同样是减少了STW的时间
三. 垃圾收集器的分类
-
线程数分: 并行和串行
单个CPU 多个CPU参与垃圾回收, 单CPU处理器或者较⼩内存等硬件场合中例如客户端Client模式下的JVM
-
按照⼯作模式分: 并发式和独占式
并发式: 运⾏时应⽤程序线程交替⼯作,以尽可能减少应⽤程序的停顿时间。
独占式: 运⾏时就停⽌应⽤程序中的所有⽤户线程,直到垃圾回收过程完全结束。
-
按碎⽚处理⽅式分: 压缩式和⾮压缩式
压缩式垃圾回收器不会产生垃圾碎片 ,非压缩式反之不整理,则会产生碎片
四. 如何评估垃圾收集器的性能
-
吞吐量: 指代码运行的时间比例 代码运行时间/(GC时间+代码运行时间)
-
低延迟: STW暂停时间
-
堆内存大小: Java堆的大小
以上三者也是一个不可能三角问题, 我们尽量做到 在最⼤吞吐量优先的情况下,降低停顿时间
五. 常见的垃圾回收器
串⾏回收器:Serial(复) 、 Serial Old(标清)
并⾏回收器:ParNew(复)、Parallel Scavenge(复)、Parallel Old(标整)
并发回收器:CMS(并发标清)、G1(区域化分代/标记-整理)
5.1 Serial/Serial Old(复制算法/标记-整理算法)
Serial/Serial Old 是最老一代的垃圾收集器
Serial: 复制算法,收集器是作为HotSpot中Client模式下的默认新⽣代垃圾收集器。
Serial Old: 标记-整理算法,收集器是针对⽼年代的收集。
只利用单核, 所以省去了CPU切换的成本, 只适用于Clinet模式或者单核服务器 -XX:+UseSerialGC 开启 Serial + Serial Old
5.2 ParNew (复制算法)
Par是Parallel的缩写,New:只能处理的是新⽣代.Serial的多线程版, 这样对于新⽣代,回收次数频繁,使⽤并⾏方式⾼效. 在多核服务器的情景下使用, 且CMS只能与ParNew配合使用.
-XX:+UseConcMarkSweepGC 老年代用CMS
-XX:+UseParNewGC 新生代用ParNew
5.3 Parallel Scavenge(复制算法)
ParNew 的可控制吞吐量的 "吞吐量优先的垃圾收集器" , 能⾃适应调节CPU利用时间, 尽力的完成掉GC过程, 减少CPU切换适用于高吞吐量的场景, 长时间计算的场景, 例如: 批量处理、订单处理
-XX:+UseParallelGC Parallel Scavenge 作为新⽣代收集器
-XX:+UseParallelOldGC ⼿动指定⽼年代使⽤并⾏回收收集器。
5.4 Parallel Old (标记-整理算法)
⽼年代并⾏收集器,吞吐量优先,Parallel Scavenge收集器的⽼年代版本;与Parallel Scavenge收集器搭配使⽤;注重吞吐量。jdk7、jdk8 默认使⽤该收集器作为⽼年代收集器
-XX:+UseParallelOldGC 来指定使⽤ Paralle Old 收集器。
5.5 CMS(Concurrent Mark Sweep)收集器(标记-清除算法)
在JDK1.5时,HotSpot推出了⼀款在强交互应⽤中并发收集器垃圾收集线程与⽤户线程同时⼯作
由于允许GC时 用户线程同步并发进行, 所以STW的时间大大减少, 使得延迟降低, 主要用于面向B/S系统中
5.5.1 收集步骤
-
初始标记: 速度最快的 标记能被 GC Root关联到的对象作为保留 -
并发标记**(无STW)**: 遍历整个内存, 标记出要回收的对象 -
重新标记: 标记一下在 并发阶段 对象发生变动的, 这里也会稍微STW一下, 只是也不会那么慢 -
并发清除**(无STW)**: 清理删除标记阶段判断的已经死亡的对象,释放内存空间。不移动存活对象
由于最耗费时间的并发标记与并发清除阶段都不需要暂停⼯作,所以整体的回收是低停顿的。
另外,由于在垃圾收集阶段⽤户线程没有中断,所以在CMS回收过程中,还应该确保应⽤程序⽤户 线程有⾜够的内存可⽤。
因此,CMS收集器不能像其他收集器那样等到⽼年代集合完全被填满了再进⾏ 收集,⽽是当堆内存使⽤率达到某⼀阀值时,便开始进⾏回收。以确保应⽤程序在CMS⼯作过程中依然 有⾜够的空间⽀持应⽤程序运⾏。
要是CMS运⾏期间预留的内存⽆法满⾜程序需要,就会出现⼀ 次"Conrurrent Mode Failure"失败,这时虚拟机将启动后备预案:临时启动Serial Old收集器来重进⾏⽼年的垃圾收集,这样停顿时间就很⻓了。
5.5.2 空闲列表 和 指针碰撞
-
空闲列表:
由于没有进行压缩整理, 在标记清除, 此时虚拟机就需要知道从哪些位置开始分配新内存. 那么就记录了一段列表来记录哪些位置有碎片空间
-
指正碰撞
5.5.3 优缺点
优点: 1.并发收集 2.低停顿, 响应快
缺点:
-
并发GC时,CPU占用给GC, 所以吞吐量低 -
无法处理浮动垃圾 -
由于碎片过多, 假设碎片很零散(总体空间空间还有很多), 没有一个较大的连续空间, 此时一个较大的对象需要分配的话很容易频繁的再次触发GC
5.5.4 参数设置
-XX:+UseConcMarkSweepGC 手动启动CMS收集器, 此时参数-XX:+UseParNewGC自动打开。
即:ParNew(Young区) + CMS(Old区) + Serial Old的组合
5.6 G1(Garbage First)收集器 (区域化分代式/标记-整理)
为了在不断扩⼤的内存和不断增加的处理器数量,进⼀步降低STW,同时兼顾良好的吞吐量。G1 是在延迟可控的情况下获得尽可能⾼的吞吐量 , 此它是⼀款并⾏与并发收集器,并且它能建⽴可预测的停顿时间模型
G1是⼀款⾯向服务端应⽤的垃圾收集器,主要针对配备多核CPU及⼤容ᰁ内存的机器,以极⼤概率 满⾜GC停顿时间的同时,还兼具⾼吞吐量的性能特性。
G1 GC有计划的避免在整个java堆中进⾏全区域的垃圾收集。G1 跟踪各个Region⾥⾯的垃圾堆积 的价值⼤⼩(回收所获得的空间⼤⼩以及回收所需时间的经验值),在后台维护⼀个优先列表,每次根据 允许的收集时间,优先回收价值最⼤的Region。
5.5.2 优缺点
优点:
-
并⾏: 垃圾回收时可以有多个GC线程同时并发回收, 利用多核心 -
并发: 用户线程和GC线程并发执行, 降低STW时间 -
能预测最优先GC的目标Regions, 避免回收全部内存空间
缺点:
-
消耗内存 和 CPU 的负载较高
整体⽽⾔:⼩内存应⽤上,CMS ⼤概率会优于 G1;
⼤内存应⽤上,G1 则很可能更胜一筹。
这个临界点⼤概是在 6~8G 之间(经验值)
5.5.5 设置
G1的设计原则就是简化JVM性能调优,只需要简单三步即可完成:
第⼀步: 开启G1垃圾收集器 (-XX:+UseG1GC)
第⼆步:设置堆的最⼤内存( -Xmx -Xms)
第三步:设置最⼤停顿时间(‐XX:MaxGCPauseMillis)
5.5.6 收集过程:
-
初始标记(STW):标记出 GC Roots 直接关联的对象, 这个过程最快, 需要停⽌⽤户线程,单线程执⾏。 -
并发标记:从 GC Root 开始对堆中的对象进⾏可达性分析,找出存活对象,这个阶段耗时较⻓,但可以和⽤户线程并发执⾏ -
最终标记: 修正在并发标记阶段由于⽤户程序执⾏⽽产⽣变动的标记记录。 -
筛选回收(STW): 根据用户设置的期望的 GC停顿时间, 计算各个 Region 的回收价值和成本进⾏排序. 这里依旧使用STW尽快完成回收过程
使用场景
-
大内存, 多CPU的服务器中 (内存和CPU太小反而不乐观) -
在大内存的情况下, 每次回收并不会全部进行回收, 而是部分回收, 所以STW时间短
七. 小结
发展过程: Serial => Parallel(并⾏) => CMS(并发) => G1 => ZGC
垃圾收集器 | 分类 | 作⽤位置 | 使⽤算法 | 特点 | 使⽤场景 |
---|---|---|---|---|---|
Serial | 串⾏ | 新⽣代 | 复制算法 | 响应速度优先 | 适⽤于单CPU环境下client模式 |
ParNew | 并⾏ | 新⽣代 | 复制算法 | 响应速度优先 | 多CPU环境下Server模式下与 CMS配合使⽤ |
Parallel | 并⾏ | 新⽣代 | 复制算法 | 吞吐量优先 | 适⽤于后台运算⽽不需要太多交 互的场景 |
Serial Old | 串⾏ | ⽼年代 | 标记-压缩 | 响应速度优先 | 适⽤于单CPU环境下client模式 |
Parallel Old | 并⾏ | ⽼年代 | 标记-压缩 | 吞吐量优先 | 适⽤于后台运算⽽不需要太多交 互的场景 |
CMS | 并⾏ | ⽼年代 | 标记-清除 | 响应速度优先 | 适⽤于互联⽹或B/S业务 |
G1 | 并发、 并⾏ | 新、⽼ | 复制;标记清除 | 响应速度优先 | ⾯向服务端应⽤ |
-
最小化的使用内存和CPU: Serial Old(⽼年代) + Serial(年轻代) -
最大化的提升程序吞吐量: Parallel Old(⽼年代) + Parallel(年轻代) -
最⼩化GC的中断或停顿时间: 请选择CMS(⽼年代) + ParNew(年轻代)
JDK9新特性:CMS被标记废弃(Deprecate)了
JDK14新特性:删除CMS垃圾回收器,移除CMS垃圾收集器,如果在JDK14中使⽤默认的GC
以上是关于GC 垃圾回收器的主要内容,如果未能解决你的问题,请参考以下文章
53.垃圾回收算法的实现原理启动Java垃圾回收Java垃圾回收过程垃圾回收中实例的终结对象什么时候符合垃圾回收的条件GC Scope 示例程序GC OutOfMemoryError的示例