《性能调优之JVM》❤️05深入了解垃圾收集算法和垃圾收集器


❤️作者主页: 温文尔雅的清欢渡

❤️ 近期学习方向:性能调优

❤️欢迎 点赞 👍 收藏 ⭐ 留言 📝 关注 ✌ 私聊我

前言

深入理解垃圾收集算法和垃圾收集器。

一、垃圾收集算法

在进行垃圾回收的第一步就是要判断对象是否存活。

判断对象是否存活的算法

①引用计数算法
给对象中添加一个引用计数器,当有一个地方引用它时,计数器就会+1;当变量被释放引用或者断开引用时,计数器就会-1;如果引用计数器变为0,就代表这个对象不存活了,会被垃圾回收。

优点:实现简单,效率高。
缺点:很难解决对象之间相互循环引用的问题。
如下面代码所示:父对象有一个子对象的引用,子对象反过来引用父对象。除了对象objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,所以导致它们的引用计数器不可能为0,于是引用计数算法不能回收他们。

public class GcTest {
		Object instance = null;

    public static void main(String[] args) {
        GcTest object1 = new GcTest();
        GcTest object2 = new GcTest();
          
        object1.instance = object2;
        object2.instance = object1;
          
        object1 = null;
        object2 = null;
    }
}

②可达性分析算法
将“GC Roots” 对象作为起点,从这些节点开始向下寻找引用的对象,然后把找到的对象都标记为非垃圾对象。当所有的引用节点寻找完毕之后,其余未标记的对象都是垃圾对象,会被回收。

GC Roots根节点:虚拟机栈中引用的对象(本地变量)、方法区中类静态属性引用的对象(静态变量)、方法区中常量引用的对象、本地方法栈中引用的对象等等。

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。

  1. 强引用:普通的变量引用
    无论引用计数算法还是可达性分析算法都是基于强引用而言的。
public static User user = new User();
  1. 软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
public static SoftReference<User> user = new SoftReference<User>(new User());
  1. 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
public static WeakReference<User> user = new WeakReference<User>(new User());
  1. 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。

常用的垃圾收集算法

③标记-清除算法
分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象,反之也可。采用从GC Roots进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。
缺点: 1. 效率问题 (如果需要标记的对象太多,效率不高) 2. 空间问题(标记清除后会产生大量不连续的碎片)

④复制算法
为了解决标记-清除算法的问题,复制算法出现了。首先把内存分成大小相同的两块,对象面和空闲面,每次使用其中一块,当内存满了时,基于copying算法的垃圾收集就从GC Roots中寻找非垃圾对象,并将复制到空闲面,然后把对象面的垃圾对象都清理掉。最后,原来的空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
缺点:浪费空间。

⑤标记-整理算法
采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收垃圾对象后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。
缺点:标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此效率不高。
优点:解决了内存碎片的问题。

⑥分代收集算法
核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域,不同区域也使用不同垃圾收集算法。一般情况下将堆区划分为老年代和新生代。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意:“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。

二、垃圾收集器

如果垃圾收集算法是方法论,垃圾收集器就是具体实现。我们要根据具体应用场景选择适合的垃圾收集器。
在这里插入图片描述
Serial、Parallel 、ParNew收集器:新生代采用复制算法,老年代采用标记-整理算法。
CMS: 标记-清除算法
Parallel是JDK8默认的垃圾收集器。
G1是JDK9默认的垃圾收集器。

Serial收集器(-XX:+UseSerialGC(年轻代), -XX:+UseSerialOldGC(老年代))

Serial是串行、单线程的收集器,只会使用一条垃圾收集线程,在进行垃圾收集时必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))

Parallel是多线程的收集器,关注吞吐量和CPU资源(如何高效率的利用CPU)。默认的收集线程数跟cpu核数相同,当然也可以用参数(- XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

ParNew收集器(-XX:+UseParNewGC)

ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。

CMS收集器(-XX:+UseConcMarkSweepGC(老年代))

CMS(重点)是一种以获取最短回收停顿时间为目标的并发收集器,关注点更多的是减少用户线程的停顿时间来提高用户体验,让垃圾收集线程与用户线程基本上同时工作。
执行过程分为四个步骤:
初始标记(STW): 暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快。
并发标记: 从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
重新标记(STW): 为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,主要用到三色标记里的增量更新算法做重新标记。
并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。如果有新增对象会被标记为黑色不做任何处理。
并发重置:重置本次GC过程中的标记数据。

CMS的相关核心参数

  1. -XX:+UseConcMarkSweepGC:启用cms
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比) 6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整 7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段 8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW 9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

优点:并发收集、低停顿。
缺点(重点):
1.CPU资源敏感:用户线程和垃圾收集线程会争抢资源;
2.会产生浮动垃圾:在并发清理阶段产生的垃圾,只能等到下一次gc再清理了;
3.空间碎片:收集结束时会有大量空间碎片产生,当然通过参数- XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理 ;
4. 并发失败:执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入STW,用serial old垃圾收集器来回收。

G1收集器(-XX:+UseG1GC) Garbage First 垃圾优先

他的内存模型是实际不分代,但是逻辑上是分代的。在内存模型中,对于堆内存就不再分老年代和新生代,而是划分成一个个的小内存块,叫做Region。每个Region可以隶属于不同的年代。
GC分为四个阶段:
第一:初始标记 标记出GCRoot直接引l用的对象。STW
第二:标记Region,通过RSet标记出上一个阶段标记的Region引l用到的Old区Region。
第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,而只需要遍历第二步标记出来的
Region。
第四:重新标记:跟CMS中的重新标记过程是差不多的。
第五:垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶
段,G1只选择垃圾较多的Region来清理,并不是完全清理。
特点:
并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop- The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式 让java程序继续执行。
分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部 上来看是基于“复制”算法实现的。
可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了 追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"- XX:MaxGCPauseMillis"指定)内完成垃圾收集。

ZGC收集器(-XX:+UseZGC)

ZGC收集器是一款基于Region内存布局的, 暂时不设分代的, 使用了读屏障、 颜色指针等技术来实现可并发的标记-整 理算法的, 以低延迟为首要目标的一款垃圾收集器。
目标:
支持TB量级的堆。
最大GC停顿时间不超10ms。目前一般线上环境运行良好的JAVA应用Minor GC停顿时间在10ms左右, Major GC一般都需要100ms以上(G1可以调节停顿时间,但是如果调的过低的话,反而会适得其反),之所以能 做到这一点是因为它的停顿时间主要跟Root扫描有关,而Root数量和堆大小是没有任何关系的。
奠定未来GC特性的基础。
最糟糕的情况下吞吐量会降低15%。

如何选择垃圾收集器

  1. 优先调整堆的大小让服务器自己来选择
  2. 如果内存小于100M,使用串行收集器
  3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
  4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选
  5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器
  6. 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱喝皮蛋瘦肉粥的小饶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值