JVM垃圾回收篇

2021/7/22 6:06:05

本文主要是介绍JVM垃圾回收篇,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人。

文章不定期同步公众号,还有各种一线大厂面试原题、我的学习系列笔记。

基础概念

  • GC=jvm垃圾回收,垃圾回收机制是由垃圾回收器Garbage Collection来实现的。进行GC的线程是后台的守护进程(后台运行、执行特定任务),它是一个低优先级进程;当可用内存低到一定程度时,自动运行(自启),从而实现对内存的回收,故垃圾回收的时间不确定;
  • 浮动垃圾:jvm回收垃圾的同时,垃圾还在尝试产生,这些就是"浮动垃圾",即“floating Garbage”,只能等到下次GC的时候清理
  • 并行收集:垃圾回收器工作时,用户线程停止,只有多个垃圾回收线程在并行地收集垃圾
  • 并发收集:垃圾回收时,用户线程不停止,而是直接和垃圾回收线程并发地工作

如何判断对象是否是垃圾?

两种方法:引用计数法、可达性分析法(常用这种)

  • 引用计数法

每个对象都有一个引用计数器,当有地方引用该对象时,计数器+1;删除引用或引用失效时,计数器-1;当计数器=0时,说明对象没有被引用则可以被JVM回收;

优点:实现简单,判断效率高

缺点:不能解决循环引用问题

  • 可达性分析

jvm中有一部分对象可被称为【GC roots对象】,如:被栈中引用的对象、被常量引用的对象、被静态变量引用的对象,从【GC roots对象】开始向下遍历,遍历过程中与【GC roots对象】形成通路的对象则为'可达'对象=存活的对象=尚存在引用的对象,否则为'不可达'对象=死亡的对象=不存在引用的对象,当某个对象不可达时,说明对象没有被引用则可以被JVM回收

当对象被判定为垃圾对象后有何方法回收?

5种gc算法

  • 引用计数法

每个对象都会有一个引用计数器,对象增加一个引用则计数器+1,减少一个引用则计数器-1,垃圾回收时,只回收计数器为0的对象。
缺点:无法处理循环引用的情况

  • 标记清除算法

分为标记阶段和清除阶段,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象,而未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。
缺点:标记清除后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后程序运行过程中需要分配较大内存对象时,无法找到足够的连续的内存

  • 标记整理算法

标记过程仍然与"标记清除"算法一样,但是后续步骤不是直接对未标记的对象进行清除,而是先让所有标记的对象都向一端移动,然后直接清除掉该端边界以外的内存,解决了没有足够大的连续内存的问题

  • 复制算法

将可用的内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将活着的对象复制到另外一块上面,然后再把使用过的那一半内存空间一次清理掉。每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。
缺点:可用内存缩小为原来的一半。

  • 分代收集算法

java堆分成新生代和老年代,分带收集算法=新生代采用赋值算法+老年代采用标记整理算法。在新生代中,每次垃圾回收时都发现大批对象的死去,只有少量存活,故采用复制算法可以只付出少量存活对象的复制成本就可以完成收集;而老年代中因为对象存活率高,没有足够的空间让出来作垃圾回收,故使用“标记整理”算法来进行回收。

谁来回收?

用7种垃圾收集器来回收,可以对应地记忆:

串行收集器 并行收集器 吞吐量收集器
串行老年代收集器 CMS收集器 吞吐量老年代收集器
G1收集器

如上,串行收集器<->串行老年代收集器

吞吐量收集器<->吞吐量老年代收集器

并行收集器<->CMS收集器

再加上一个G1收集器,共7种收集器,这7种收集器可分别作用于新生代和老年代:

image
图中上下半部为新生代、老年代对应可用的收集器,连线代表可以结合使用

  • 串行收集器(Serial收集器)
    • 作用于新生代,故使用复制算法;
    • 仅仅使用一个线程完成垃圾收集工作;
    • 在垃圾收集时必须暂停其他所有的工作线程(stop-the-world),直到垃圾收集结束;
    • 专心做垃圾收集故效率比较高,使用【-XX:+UseSerialGC】打开

image

  • 并行收集器(ParNew收集器)
    • 作用于新生代,故使用复制算法;
    • 并行收集器其实是串行收集器的多线程版本,与Serial收集器唯一不同的地方就是在垃圾收集过程中使用多个线程并行收集,其他全部和串行收集器相同
    • 它默认开启的收集线程数与CPU的数量相同
    • 使用【-XX:UseParNewGC】打开;

除了Serial收集器,就只有它能和CMS收集器混合使用
image

  • 吞吐量收集器(ParallelScavenge 收集器)

    • 作用于新生代,故使用复制算法;
    • 吞吐量收集器的目的是追求高吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间);
    • 使用【-XX:+UseParallelGC】打开;使用【-XX:MaxGCPauseMillis】指定垃圾收集最大停顿时间,但并非越小越好;
    • 使用多个GC线程来完成垃圾收集
    • 会引起stop-the-world(垃圾收集停顿时间:垃圾收集的时候,所有java线程被挂起,除了垃圾收集器的线程,此时navtive相关代码可执行但不能与JVM交互,故stop-the-world是大家的敌人)
    • 最大的特点是'GC自适应策略'':打开GC自适应策略【-XX:+UseAdaptiveSizePolicy】,则不用再手动设置'年轻代:老年代'的大小、'Eden区:survivor区'的大小、对象晋升老年代的年龄,系统会根据实时性能去动态设置这些参数以便达到最高的吞吐量、最优的停顿时间
      image
  • 串行老年代收集器(Serial Old收集器)

    • 因为作用于老年代,所有采用'标记整理'算法
    • 和串行收集器一样,只是它作用于老年代,是串行收集器的老年代版本

image

  • CMS收集器

    • 作用于老年代,故使用'标记整理'算法;
    • 以获取最短的垃圾收集停顿时间为目标,重视响应速度和用户体验的应用;
    • CMS收集器的内存回收过程是与用户线程一共并发执行的;
    • 使用【-XX:+UseConcMarkSweepGC】打开;
    • 步骤:初始标记->并发标记->重新标记->并发清除,初始阶段=标记由【GC roots】直接到达的对象,并发标记=标记由【GC roots】间接到达的对象,重新标记=修正在'并发标记'期间由于用户程序运行而导致标记变动的标记,并发清除=清除已标记的对象

image

  • 吞吐量老年代收集器(Parallel Old收集器)
    • 因为作用于老年代,所有采用'标记整理'算法
    • 和吞吐量收集器一样,只是它作用于老年代,是吞吐量收集器的老年代版本
    • 使用【-XX:+UseConcMarkSweepGC】打开

image

  • G1收集器
    • 可作用于年轻代/老年代,jdk1.9之后的默认垃圾收集器
    • 使用【-XX:+UseG1GC】打开
    • G1收集器的内存回收过程是与用户线程一起并发执行的;
    • G1收集器不会产生内存碎片,会把内存碎片收集后形成大的内存块
    • G1收集器最大的特点是把堆的内存分成一块块大小相同的区域(region),保留年轻代、老年代的概念,但年轻代、老年代也是被清晰地分为一个个区域
    • G1追求最小的gc停顿时间,并且可用参数指定gc停顿时间,也就是建立了可以预测的gc停顿时间模型,原理:因为G1收集器把堆分成了一块块大小相同的区域,每个区域可以回收的空间大小不同,G1会在后台把每个区域的回收价值(回收价值=可回收的空间大小+回收时间成本)由大到小维护成一张优先级列表,回收是先回收价值大的区域;每个区域的大小可以通过【-XX:G1HeapRegionSize】参数指定,大小区间最小1M、最大32M,一定要是2的幂次方,默认把堆内存按照2048份均分,每个区域被标记了E、S、O和H,这些区域在逻辑上被映射为Eden、Survivor、老年代Old和Humongous去(用来存放大对象)
    • 步骤:初始标记->并发标记->重新标记->筛选回收,初始阶段=标记由【GC roots】直接到达的对象,并发标记=标记由【GC roots】间接到达的对象,重新标记=修正在'并发标记'期间由于用户程序运行而导致标记变动的标记,筛选回收=对各个region计算回收价值(回收价值=可回收的空间大小+回收时间成本),并按照用户设置的停顿时间来制定回收计划

image

image

默认的垃圾回收器

jdk1.7和jdk1.8: 年轻代默认'吞吐量垃圾收集器'(即Parallel Scavenge), 老年代默认'吞吐量老年代收集器'(即Parallel Old)

jdk1.9:默认G1垃圾收集器

OK,如果文章哪里有错误或不足,欢迎各位留言。

创作不易,各位的「三连」是二少创作的最大动力!我们下期见!



这篇关于JVM垃圾回收篇的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程