垃圾回收算法以及常用的垃圾回收器
四种垃圾收集算法:
1.标记清除算法:
算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高
它主要会带来的问题是空间问题(会产生大量的不连续的碎片)
2.复制算法:
为了解决内存空间碎片化问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
3.标记整理算法:
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法:
主要是为了利用上面几种算法的优点
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
常用的垃圾回收器:
1.Parallel收集器(年轻代和老年代都会有各自的那种)
也是利用多CPU利用多线程去并行回收对象
在垃圾回收的时候有一个吞吐量的概念:
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即
吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
当某种情况我们不追求用户交互的时候,我们其实可以采用Parallel收集器来提高吞吐量
2.CMS:是一款并发,基于标记清除算法的垃圾回收器。
主要优点就是并发收集,追求低停顿时间。(这是它最关键的一个地方)
什么时候使用CMS收集器比较好?
如果你的应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU(也就是硬件牛逼),那么使用CMS来收集会给你带来好处。还有,如果在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS。
缺点:
1.无法处理浮动垃圾(在第四个阶段并发清除的时候,同时运行的用户线程可能在继续产生需要回收的对象,这一部分对象就只能等到下一波垃圾回收的时候才能被清除,因此我们叫它浮动垃圾),在最后一个阶段仍然可能出现用户进程修改引用的情况。
2.另外因为它采用的标记清除算法所以会出现大量的空间碎片。(这里为什么不采用Compact算法呢,主要是在并发清除阶段,我们要保证用户进程的内存不能被占用,所以只能用标记清除这种方式)
3.对CPU资源敏感,为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切换是不靠谱的。
它的触发主要分为:周期性Old GC(被动)和主动Old GC:
周期性Old GC:执行的逻辑也叫 BackgroundCollect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。
主动Old GC:System.gc();
整个过程:
1.初始标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快
2.并发标记:该阶段GC线程和应用线程并发执行,遍历初始阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。
3.并发预清理:并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段”重新标记”的工作,因为下一个阶段会Stop The World。
4.重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,因为跨代引用的存在,CMS既需要扫描老年代也需要扫描年轻代。
只有Minor GC时才会使用根搜索算法,标记新生代对象是否可达,也就是说虽然一些对象已经不可达,但在Minor GC发生前不会被标记为不可达,CMS也无法辨认哪些对象存活,只能全堆扫描(新生代+老年代)。由此可见堆中对象的数目影响了Remark阶段耗时。 分析GC日志可以得出同样的规律,Remark耗时>500ms时,新生代使用率都在75%以上。这样降低Remark阶段耗时问题转换成如何减少新生代对象数量。
新生代中对象的特点是“朝生夕灭”,这样如果Remark前执行一次Minor GC,大部分对象就会被回收。CMS就采用了这样的方式,在Remark前增加了一个可中断的并发预清理(CMS-concurrent-abortable-preclean),该阶段主要工作仍然是并发标记对象是否存活,只是这个过程可被中断。此阶段在Eden区使用超过2M时启动,当然2M是默认的阈值,可以通过参数修改。如果此阶段执行时等到了Minor GC,那么上述灰色对象将被回收,Reamark阶段需要扫描的对象就少了。除此之外CMS为了避免这个阶段没有等到Minor GC而陷入无限等待,提供了参数CMSMaxAbortablePrecleanTime ,默认为5s,含义是如果可中断的预清理执行超过5s,不管发没发生Minor GC,都会中止此阶段,进入Remark。
根据GC日志红色标记2处显示,可中断的并发预清理执行了5.35s,超过了设置的5s被中断,期间没有等到Minor GC ,所以Remark时新生代中仍然有很多对象。对于这种情况,CMS提供CMSScavengeBeforeRemark参数,用来保证Remark前强制进行一次Minor GC。
在上面的minorGC中老年代也可能持有年轻代的引用,那个minorGC也会扫描老年代吗?
卡表的具体策略是将老年代的空间分成大小为512B的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示,卡表3被标记为脏(卡表还有另外的作用,标识并发标记阶段哪些块被修改过),之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描。
5.并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫。
6.并发重置 :这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收
G1一款面向服务端应用的垃圾收集器:
1.G1结构:
每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
G1则是把内存分为多个大小相同的区域Region,每个Region拥有各自的分代属性(年轻代),但这些分代不需要连续,每个Region是逻辑连续的一段内存。
常用的几个命令参数
使用G1: -XX:+UseG1GC
设置Region大小:R-XX:G1HeapRegionSize
参数指定,大小区间只能是1M、2M、4M、8M、16M和32M,总之是2的幂次方
G1收集过程的时间:-XX:MaxGCPauseMillis
2.优点:
并行与并发:
G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop The World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
可预测的停顿:
G1除了追求停顿外,还能建立可预测的停顿时间模型,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据运营的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。
G1通过将内存空间分成区域(Region)的方式避免内存碎片问题,Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
避免全堆扫描:(CMS第四阶段垃圾回收的时候,可能会在不同的呆里面穿梭,导致效率低下)
为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
3.GC模式
young gc
发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者直接晋升到old region中(survivor的块不够你存放时),空闲的region会被放入空闲列表中,等待下次被使用。
G1 YoungGC在Eden充满时触发,在回收之后所有之前属于Eden的区块全变成空白。然后至少有一个区块是属于S区的(如图半满的那个区域,除了从E区移过来的一部分,还有它自己剩的一部分),同时可能有一些数据移到了O区。
mixed gc
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。
这个阶段的过程:
1.initial mark: 初始标记过程,整个过程STW,标记了从GC Root可达的对象
2.concurrent marking: 并发标记过程,整个过程gc collector线程与应用线程可以并行执行,对GC Root进行可达性分析,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息,这个阶段会发生young gc的。
- Young区发生了变化、这意味着在G1并发阶段内至少发生了一次YGC(这点和CMS就有区别),Eden在标记之前已经被完全清空,因为在并发阶段应用线程同时在工作、所以可以看到Eden又有新的占用
- 一些区域被X标记,这些区域属于O区,此时仍然有数据存放、不同之处在G1已标记出这些区域包含的垃圾最多、也就是回收收益最高的区域
- 在并发阶段完成之后实际上O区的容量变得更大了(O+X的方块)。这时因为这个过程中发生了YGC有新的对象进入所致。此外,这个阶段在O区没有回收任何对象:它的作用主要是标记出垃圾最多的区块出来。对象实际上是在后面的阶段真正开始被回收
3.remark: 最终标记过程,整个过程STW,用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到该Regin的Remembered Set中,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象。
4.clean up: 最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中
讲的挺好
https://crowhawk.github.io/2017/08/15/jvm_3/
像普通的YGC那样、G1完全清空掉Eden同时调整survivor区。另外,两个标记也被回收了(有一个E区和X区被清空了),他们有个共同的特点是包含最多可回收的对象,因此这两个区域绝对部分空间都被释放了。这两个区域任何存活的对象都被移到了其他区域(和YGC存活对象晋升到O区类似)。这就是为什么G1的堆比CMS内存碎片要少很多的原因–移动这些对象的同时也就是在压缩对内存(标记清除阶段会STW)。
对于上面两个阶段:
每次混合GC只是清理一部分的O区内存,整个GC会一直持续到几乎所有的标记区域垃圾对象都被回收,这个阶段完了之后G1会重新回到正常的YGC阶段。周期性的,当O区内存占用达到一定数量之后G1又会开启一次新的并行GC阶段.
full gc
如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.
ZGC:款低停顿高并发的收集器。
ZGC主要新增了两项技术,一个是着色指针Colored Pointer,另一个是读屏障Load Barrier(来保证在Compact阶段也不会stw)。
本文来自博客园,作者:LeeJuly,转载请注明原文链接:https://www.cnblogs.com/peterleee/p/10528468.html