目标

  • 熟悉GC常用算法,熟悉常见垃圾收集器,具有实际JVM调优实战经验
  • 垃圾:没有任何引用指向的对象就是垃圾

image.png

  • 专门的人清理垃圾的时候也要算进执行效率中去,所以执行效率偏低
  • 如何找到垃圾:
    • 引用计数—->不能解决循环引用,会造成一团垃圾、一堆垃圾无法回收!!!
    • 根可达算法:从根上对象开始搜,首先扔的几个对象就算是根对象,后面连上去的就不算是根对象了
  • 根对象的定义:(四类)线程栈变量、静态变量、常量池、JNI指针
    • 一个main方法开始运行,会起一个main线程,线程中有栈,栈中有对应的栈帧,从main栈帧中开始的这些对象都是根对象,叫线程栈变量;main线程栈引用了别的方法,别的方法一定也是能够引用到的(是有用的对象),main线程栈帧中开始的那些对象是根对象
    • 静态变量—->T.class,class加载到内存中后马上就要对静态变量进行初始化,所以静态变量能够访问到的对象就是(根对象)
    • 常量池指这个class会用到其他的class那些类的对象是根对象
    • C/C++写的那些本地方法所用到的类或者对象是根对象
    • 包括普通对象和类对象

image.png

  • 垃圾就是没有任何引用指向的对象或者一堆对象—->reference count和root searching两种判断方式(根可达算法与引用计数)
  • python用的就是引用计数,内部怎么解决循环引用不清楚?❓

GC算法(算法扫描标记的是不可回收的对象)

  • 标记-清除Mark-Sweep
    • 找到没用的标记出来,清理掉
    • 相对简单,在存活对象比较多的情况下效率比较高
    • 问题(算法上的小问题,不要和效率高混在一起):
      • 需要经过两遍扫描,执行效率低
      • 容易产生碎片
    • 不适合eden区,因为伊甸区中存活的对象本身比较少,不适合这种算法
    • 不压缩、不整理中间会产生窟窿
    • 第一遍扫描找出有用的,第二遍对那些没用的进行清除、清理—->这是这种算法的特点,也不能算是缺点,每一种算法都有相应的优缺点
  • 拷贝Copy
    • 将一块区域有用的整齐地拷贝到另一边去,拷贝完之后之前一块区域的全都清理掉
    • 清楚明白,挨在一起,没有碎片,很少有
    • GC的root找到那些有用的,将那些有用的拷贝到那块内存中去—->会发生对象的移动过程
    • 有对象的移动和复制,所以引用也需要调整,引用的值得跟着动
    • 在java中即使是同一个对象的引用,这个引用在任何时候也未必完全一样
    • 缺点:
      • 空间浪费
      • 移动复制对象,需要对象引用的调整
    • 适用于存活对象较少的情况
    • 只扫描一次,效率提高—->所以这种算法适合用在伊甸区
    • 没有碎片
  • 标记-压缩Mark-Compact
    • 把所有对象的清理整理过程同时压缩到头上去
    • 有用的全往前面走,哪怕是空着的,先压到前面去—->整理到房间的角落,剩下的就是比较大的完整的连续的没有碎片的空间
    • 通过GCRoot找到不可回收的得往前挪,挪到前面的位置上去,因为之前的可能是垃圾,把垃圾填了
    • 肯定要扫描两次,并且还要进行对象的移动
      • 第一遍找出有用的
      • 第二遍进行移动
      • 移动的过程中如果使用多线程移动,还要进行多线程同步—->效率更低了
    • 扫描两次,移动对象—->效率偏低
    • 不会产生碎片,方便对象分配,并且不会产生内存减半
  • 总结


垃圾回收器是综合使用垃圾查找策略并结合垃圾回收算法进行垃圾回收的一个综合性的组件应用


JVM内存分代模型(用于分代垃圾回收算法)

  • 新生代+老年代+永久代(1.7) Perm Generation/元数据区(1.8)Metaspace
    • 永久代 元数据 - Class
    • 方法区中存的是class的源信息、代码的编译信息、各种各样的编译信息、JNI或者JIT产生的那种代码的信息
    • 永久代要设置大小,所以容易产生溢出
    • 元空间可以设置,也可以不设置;不设置的时候是没有上限的,但是依然受限于物理内存
  • 字符串常量池在1.7时在永久代(方法区)中,而1.8时放到了堆内存中
  • 方法区是堆之外的空间(堆外空间),新生代、老年代叫

堆内逻辑分区

  • 实际上jvm的hotspot用的是分代算法
  • 分代算法与垃圾回收器是有关系的
  • 不要因为jvm用了这么多年的算法就认为jvm必须用分代算法
  • 分代是存在于ZGC之前的,ZGC之前的所有的垃圾回收器都是分代算法
  • ZGC和SD这两个垃圾回收器从逻辑上来讲已经不再分代了,但是生产环境中用的太少了,主要学现在现实中用到的垃圾回收器
  • 现在现实中用到的垃圾回收器至少在逻辑上是分代的
    • 除了G1垃圾回收器,其他垃圾回收器不光在逻辑上是分代的,在物理上也是分代的
  • 部分垃圾回收器使用的模型

    • 除了Epsilon——>是用来做debug(调试用的垃圾回收器)的垃圾回收器,实际不干什么事就起个占位符的作用,以后不说他了,对我们来说也没什么用
    • 除了Epsilon、ZGC、Shenandoah之外的GC都是使用逻辑分代模型
    • G1是逻辑分代,物理不分
    • 除此之外不仅逻辑分代,而且物理分代
    • 逻辑分代指的是对内存做一些概念上的区分,这块内存叫做什么,那块内存叫做什么
    • 物理分代是真正的物理内存、这块内存空间将其划分出来区域的,这块区域确实属于年轻代,这块区域确实属于老年代(跟面试官聊的时候要区分出来,精确地说)—->高看或者培训,示弱假装不知道,不要怼面试官,怼面试官要有技巧

      逻辑分代

  • 将内存分为新生代和老年代(1:3—->除了G1、Shenandoah之类的回收器之外其他的都可以这样认为)

  • 1:3是一个默认的关系,可以通过参数来改变他❌❌❌
  • 上述的比例关系应为1:2
  • new/young新生代:刚刚诞生出来的对象(new出来的对象)
    • 又分为伊甸Eden区和两个幸存survivor区
    • 默认比例为8:1:1
    • 伊甸区是真正new出来的对象进入的区域,而幸存区是经过一次回收之后进入的区域
    • 幸存区可以叫S1、S2也可以叫from survivor、to survivor
  • old/tenured老年代:垃圾回收了很多次都没有被回收掉的老顽固
  • 因为新生代和老年代中装的对象不同,所以里面采用的垃圾回收算法也不同
    • 新生代存活对象特别少,死去对象特别多,用拷贝算法
    • 老年代活着的对象特别多,所以适用标记-清理或者标记-整理(压缩)算法

image.png

对象的分配周期

  • 首先尝试在栈上分配
  • 栈上分配不下,进入伊甸区
  • 经过垃圾回收之后进入幸存区
  • 在之后的几次回收之中,对象会从一个幸存区进入另一个幸存区中,与此同时伊甸区中新的对象也会跟着进入这另一个幸存区中
  • 年龄超过限制之后(年龄够了)就会进入老年代
  • ❓什么时候进行栈上分配,什么时候进入伊甸区

image.png

GC概念

  • 凡是在年轻代进行回收的叫做MinorGC或者YGC:年轻代空间耗尽时触发
  • 凡是在老年代或者整个区域进行回收的叫做MajorGC或者FullGC:在老年代无法继续分配空间时触发,新生代老年代同时进行回收(回收老年代的时候顺便把新生代给回收掉了?)

image.png

栈上分配

  • 什么样的对象、东西会分配到栈上???
  • 什么样的内容会继续往伊甸上去分配
  • 上面的两块区分开做实验做不出来,只能合在一起做个小实验
  • c语言中的struct结构体,他可以直接在栈上分配
  • 为了对标这一点,java就引入了栈上分配这一理念
  • java的栈上分配的对象
    • 首先要小
    • 然后还要是线程私有
    • 无逃逸:只在某一段代码中使用,出了这段代码就没有人认识了(变量的作用域的概念—->代码块)
    • 支持标量替换:一个对象中有两个int类型的属性的时候就可以直接用两个int类型代表这个对象了,没必要再把他当成一个完整的对象了—->用普通的类型来代替整个对象就是标量替换
  • 栈上分配别随便调整,不要随便动他,没有任何意义
  • 知道这么回事

  • 当栈上分配不下了,会优先进行线程本地分配(TLAB:Thread Local Allocation Buffer)
  • TLAB:好多线程都往伊甸区中分配对象,线程之间就会进行空间的争用(谁抢到就是谁的),多线程的同步—->效率降低
  • 所以设计了TLAB,TLAB占用伊甸区,默认是1%—->每个线程在伊甸区取个1%这样的一个空间,这块空间这个线程独有(代表什么时候要在伊甸区分配对象的时候,优先往这个线程独有的空间中分配对象,这样就不会与其他线程产生空间争用的问题,效率也会随之提高
  • 栈上分配和线程本地分配这两块内容无需调整;作为调优(tuning)来讲,我们没必要去调他们—->有些文章也会去调TLAB,这也是可以的,在其他调了都没有效果的情况下,(愿意调?)可以试一下调线程本地分配(TLAB)
  • ?内部应该有机制来对他进行优化吧(猜的)

  • 为什么在栈上分配更好一些(更快),使用TLAB更好一些呢?(经实验得出有没有时间相差2倍左右!!!)
    • 很容易理解,栈上分配肯定比堆上分配要快很多
    • 并且在栈上分配,不需要使用了可以直接弹出,不需要垃圾回收的介入—————>1000个对象触发了垃圾回收的话,在栈上是不需要考虑这个问题的
    • 栈一弹出就没了,跟垃圾回收是没有关系的,不需要其他人介入了,效率因此高了一些
    • 线程本地分配不需要和其他线程去争用了,所以这个效率会相对提高

对象什么时候进入老年代—->回收了多少次之后进入老年代

  • 通过参数XX:MaxTenuringThreshold指定次数(YGC)
  • 因为GC年龄在JMM中只有4位,最大就是15,所以网上说的调到比15大是不可能的
  • CMS为什么是6,比较小一些,可能是因为CMS容易产生两种问题:PromotionFailure和ConcurrentModeFailure,如果小一些可能出问题的可能性小一些(好像也是说不通的?)
  • 参数不需要全部记住,只需要在用到的时候知道去哪里查就可以了(七八百个参数记不住)
  • 动态年龄:当垃圾回收的两个survivor之间拷贝来拷贝去超过50%(伊甸区中的对象和其中一个survivor区中的对象放到另一个survivor中去时假如超过了这另一个survivor区的一半就把年龄最大的一些对象放到老年代中去)的时候就把年龄最大的放到old区中去,不一定要等年龄达到MaxTenuringThreshold才会进入old区(不一定非得到几岁)

image.png

  • 两个不同的survivor总和大于50%????❌—->不存在这种情况,因为在任意时刻其中一个survivor必然是为空的,因为他使用的是拷贝的垃圾回收算法
  • 也不是相同年龄所有对象大小的综合大于survivor的一半,年龄大于或等于该年龄的对象就可以直接进入老年代而无序等到MaxTenuringThreshold中要求的年龄;
  • 实际上通过c++源码的解读,是所有年龄的总和加起来超过一半超过(这个阈值年龄)的那些对象就会进入老年代

image.png
image.png

  • 上图中2和3都要晋升到老年代中去
  • 累加和相同年龄是两个概念—->累加和是正解!!!
    • 分配担保
  • 阿里问多深是跟人有关系的,问到一个问题谈不下去了,就不会再往深了问了
  • 不是很重要多数人是问不到的
  • survivor区在YGC分配期间空间不够了(正在进行YGC时有新的对象进来survivor区的空间不够了),这时通过空间担保直接进入老年代(直接进入老年代的是之前在新生代中的老对象,而不是刚来的那个要申请内存的对象)
  • 内存分配担保机制就是当在新生代无法分配内存的时候,把新生代的对象转移到老生代,然后把新对象放入腾空的新生代。
  • java堆内存的分配担保

image.png

对象的分配过程

  • 能分配到栈上,就分配到栈上,分配到栈上的好处—->直接往外一弹,就可以回收结束了
  • 如果分配不下,看这个对象大不大,用一个参数来指定:PreTenuringSizeThreshold来指定;
    • 假如对象特别大的时候会直接进入old区,通过FGC来回收结束;
    • 如果对象不够大会进入TLAB(对象本地分配缓存),不管是通过TLAB还是不通过,总之全部都是在伊甸区(Eden)
  • 在伊甸区,如果进行YGC清理完了就结束,如果没有清理完就进入S1,S1再进行GC清除,如果年龄够了进old区,如果年龄不够进S2(其中也可能有动态年龄判断———->超过50%就将那些最老的对象放入old区)

JVM参数

  • 新生代和老年代之比为1:2(1.5之前可能是1:3)

image.png

  • 直接java回车会看到以横杠开头的参数,这些参数都是标准参数,这些参数所有的版本都支持
  • -X开头是非标准参数non-standard options
  • java -X就能将全部的非标准参数显示出来
  • JVM参数给的文档都不是特别全,包括英文的一些也不是特别全(没有)
  • 网上的一些博客、文章都是不全的,但是可以参考
  • 想看全的只能这么写java -XX:+PrintFlagsFinal,两个X表示不稳定的参数,表示有的版本支持这个参数,而有的版本却不支持了但支持另外的参数
  • 每个版本和每个版本是不一样的,有的在这个版本中设置好了,但另外的版本就不支持了(这里以jdk1.8为主
  • java -XX:+PrintFlagsFinal参数的含义是打印参数的最终值—->Flags的意思是**参数**的意思
  • java -XX:+PrintFlagsFinal后面可以随便跟个什么东西,或者不跟都行
  • 这条指令会打印出当前版本的所有不稳定的参数,一共七八百个—->所以要学会从这么多的参数中去找自己需要使用的参数
  • 这七八百个参数还能够进行组合
    • CMS+调试用的参数一共有二三百个
    • G1的参数比CMS少很多,大概只有一百多个
    • ZGC不知道,目前为止大部分还是实验性的
  • 以后的调优(tuning)会越来越简单,zing号称没有参数或者1个参数(收费的)


常见的垃圾回收器(实物)—->背过

image.png

  • 上图中不包括Zing,因为zing是虚拟机层面的概念,而这里是指垃圾回收器的概念
  • 虚拟机中可以使用不同的垃圾回收器,可以根据虚拟机自己的特点、需要选择合适的垃圾回收器
  • 而垃圾回收器又是对垃圾回收算法的具体实现,所以可以通过不同的方式、采用不同的垃圾回收算法来实现不同的垃圾回收器

常见垃圾回收器的历史

  • JDK诞生的时候 Serial追随—->Serial指的是单线程,parallel指的是多线程
  • JDK诞生之后第一个垃圾回收器就是Serial
  • 垃圾回收器常见的三种组合:
    • Serial组合:Serial+Serial Old—->现在不常用了
    • Parallel组合:Parallel Scavenge+Parallel Old—->现在仍然有很多的生产环境在用这两种组合,在上线之前没有做任何jvm调优,jvm设置的话就默认是这种组合PS+PO
    • ParNew+CMS组合
  • 除了上述三种组合,其他的组合是不常见;虽然也能也能组合,但是组合不常见
  • 能够组合判断的依据:凡是上图中黄线连在一起的就是能够进行组合的方式
  • 前面几种不光逻辑上分年轻代和老年代,而且在物理上也分年轻代和老年代
  • G1只是在逻辑上分年轻代和老年代,在物理上就分为一块一块的了,详见之后的G1(会全讲)—->怼怼老师、怼怼面试官
  • CMS可以和Serial Old组合在一起

    JDK诞生 Serial追随
    提高效率,诞生了PS 为了配合CMS,诞生了PN CMS是1.4版本后期引入,CMS是里程碑式的GC,他开启了并发回收的过程 但CMS毛病较多,因此目前没有任何一个JDK版本默认是CMS 但这不代表CMS不重要,CMS其实非常重要,理解CMS的概念之后,后面的G1和ZGC理解起来就会简单很多!!!

Serial

  • 垃圾回收线程是单线程
    • 这种回收器主要用在内存非常小的情况
    • JDK刚开始那会几十兆、上百兆就到头了,所以一个线程做清理没问题,在时间上能够接受
    • 但是现在的内存使用单线程的话,停顿时间是非常长的,是接受不了的
  • 垃圾回收进行的时候俗称为停顿时间
  • 当垃圾回收器干活的时候,所有的那些工作线程必须全部都停止
  • 夫妻两在房间里扔线团,剪线团,回收打扫线团的例子
  • safe point的概念(问的也不是特别多):
    • 线程停止并不是让你停你马上就能停止
    • 剪线团剪一半(对象与引用断开)不能停止,得找到一个安全点才能停止
    • 正在解锁unlock,必须等到unlock结束之后才能把他停住
    • 所以线程能够停止的位置叫做安全点
  • SWT:stop-the-world(跟面试官聊天就聊STW,不说stop-the-world)
  • Serial在现在用的是极少的,现在不常用了

image.png

Serial Old

  • 和Serial相比,就是使用的垃圾回收算法改变了
  • Serial使用的是拷贝,而Serial Old使用的是标记-清除或者标记-整理算法
  • Serial用在新生代,Serial Old用在老年代
  • STW
  • Serial Old依然是单线程的垃圾回收器
  • Serial Old在现在用的是极少的,现在不常用了

image.png

Parallel Scavenge

  • PS+PO这种垃圾回收的组合现在仍然有很多的生产环境在使用
  • 假如在正式上线之前不进行任何的jvm调优,就默认使用的是PS+PO这种组合
  • 假如能够接触到线上的生产环境可以看一看默认的垃圾回收组合是什么样的
  • 默认的垃圾回收组合在windows上比较好查(可以直接打印出来),而在Linux上查的话还得用专门的命令和工具(以后再去看)
  • 越没有把握、没有把关,越没有权限;测试环境都摸不着
  • 🌟默认的不做任何设置,不明确指定是CMS,或者不明确指定是G1的话,默认的就是PS+PO,读GC日志的时候,读的也是这两种的日志

  • 与之前的Serial唯一的区别就在于垃圾回收、清理是多线程的,是多个线程进行垃圾的清理和回收的
  • 好多个线程一块清理垃圾
  • 多线程清理垃圾

image.png

Parallel Old

  • 一样是多线程的
  • 一样是STW的
  • 但是垃圾回收算法只使用了标记-整理(标记-压缩Compacting)算法

image.png

不指定垃圾回收器的时候,默认的垃圾回收器组合就是PS+PO这种组合,读的GC日志也是这两种的日志

  • 很多其他的垃圾回收器都是以这两种为基础拓展开来的
  • 在读的时候、学习的时候就很容易读,很多概念都很容易理解了
  • 之后讲的时候就以PS+PO为基础
  • 不用再纠结单线程和多线程的区别了!

以PS+PO为基础(为例)使用各种各样的工具

  • 很多其他的垃圾回收器都是以这两种为基础拓展开来的

ParNew

  • 实际上就是Parallel New
  • 整个垃圾回收器的演化过程不是特别连贯,所以名字起的也不是特别连贯
  • 与Parallel Scavenge没有什么区别,就相当于是PS的新版本;这个才是第三个诞生的
  • STW:有没有不会产生STW的垃圾回收器?目前还没有
    • 所谓的不会产生是严格的
    • ZGC就号称目标是设计到STW的时间在10ms以下,可是实际中测的时候是2ms不到,差不多1ms左右
    • ZGC的STW的时间间隔是很牛叉的
  • 🌟与PS的区别:
    • 做了一些增强以便能让他和CMS配合使用
    • 说白了就是为了兼容CMS
  • 说白了就是为了兼容CMS而在PS的基础上做出了一些改变
  • 是Parallel Scavenge的一个变种,这个变种适合CMS配合使用的
  • 增强:
    • CMS在特定阶段的时候会和ParNew同时运行,可以并发运行
    • 做了一些和CMS相配合的一些变种
  • 默认线程数即cpu的核数

image.png

PS V.S PN(拓展延申阅读)

  • 下面的链接是oracle的解释
  • 假如被问到可以说PN是PS的变种,PN是为了配合CMS使用对PS加以修改而产生一个变种!!!

image.png

CMS

  • 是一个里程碑式的垃圾回收器
  • 其他的所有垃圾回收器干活的时候,其他的人不能干活;垃圾回收器来了,其他所有干活的垃圾回收器都会停;全都会停住,等着垃圾回收器来回收
  • 垃圾回收器回收结束走了之后才能继续干活
  • CMS的诞生就消除了上面的弊端—->concurrent mark sweep
  • 夫妻两扔线团剪线团的时候,就可以直接进来打扫剪掉的线团了;一边扔一边玩的时候垃圾回收器就可以直接进来干活了—->垃圾回收线程和工作线程同时进行(这就叫做并发concurrent,parallel叫做并行)
  • 在回收垃圾的过程中还能产生新的垃圾
  • parallel是指在垃圾回收的过程中有多个线程并行回收—->多个线程同时回收,至于有没有和其他的并行,他不强调这一点
  • 但是CMS其实毛病也非常多,CMS的毛病非常多,而且是那种搞不好的毛病,天生的毛病
  • CMS的毛病很大以至于在任何jdk版本中默认都不是CMS,Oracle也知道他的毛病很大

    为什么会需要CMS(并发垃圾回收)?

  • 使用并发垃圾回收是因为无法忍受STW,STW严重到无法忍受

  • 不管是多线程也好,单线程也好,进行垃圾回收的时间都太长了
    • 在内存比较小的情况下,可以很快清理完,给用户响应,让用户继续使用空间
    • 但是现在内存比较大的情况下,无论多少线程过来,清理一遍也得需要特别长的时间
    • 清理一遍到底要多长时间呢?(一个房间—->一个天安门广场)
      • 2分钟???不能这么说
      • 有人在10g内存的时候用PS+PO,清理一次的停顿时间大概要十几秒(11s)左右的时间
      • 假如用CMS中间产生一次……❓
      • CMS最后也会产生一个碎片(垃圾),产生碎片之后会进行FGC,那个STW最长的时间要到十几个小时,长的可能会几天,直接卡死在那,啥都不能干了(在内存特别大的时候
    • 忍受不了STW了,想要把STW的时间给缩短,这时产生了CMS
    • CMS论文发出来和真正写出来隔了好长的时间,说明东西设计出来和实现出来真正不是一回事

image.png
image.png

CMS常见的几个阶段

  • 初始标记阶段:初始标记很简单,表示直接找到最根上的对象,其他对象我不标,只把最根上的对象给标记出来,表示已经标记过了
    • 初始标记是STW
    • 但是由于只标记一些最开始的对象,所以需要的时间并不长,很短的时间就能完成
  • 并发标记阶段:有人统计过80%的GC的时间都是浪费在这个阶段,这块是最浪费时间的,因此把这块最浪费时间的标记和正常的程序一起运行、同时运行,此时应用程序不会停
    • 客户可能会感觉稍微慢一些,但是至少还会有反应
    • 并发标记阶段一边产生垃圾,一边进行标记
  • 重新标记阶段:因为上一个阶段很难全部完成标记(包括在上一个阶段中应用程序产生的新的标记),所以又有一个重新标记的过程
    • 这又是一个STW的过程
    • 多数的垃圾在并发标记的过程中标记完了,在并发过程中产生的新的垃圾在重新标记里面标记一下
    • 这时候就需要停一下,因为不停的话会不断产生新的垃圾
    • 虽然新的垃圾不多,但是要标记清楚的话得让他们两都停掉,对新产生的垃圾进行重新标记
    • 这个过程和前面的过程一样:由于新产生的垃圾并不多,要调整的标记并不多,所以这个过程停顿的时间也不长
  • 并发清理阶段:并发清理也有他的问题,他清理的过程中也会产生新的垃圾,之前标记过的可以进行清理,但是清理过程中产生的新垃圾就没办法了
    • 对于这种垃圾该怎么办?
      • 这种垃圾叫浮动垃圾
      • 只能等到下一次CMS再次开始运行的过程来把他清掉
  • 从线程的角度来理解CMS的四个阶段!!!
  • 有些资料会写六个阶段:就是说有些阶段叫做预处理pre,预标记,在并发标记之前;在并发清理完成之后可能会有个整理阶段;这是简单的,不太重要的就不多说了
  • 什么条件触发CMS?
    • 老年代分配不下了会触发CMS
    • CMS是老年代的垃圾回收器,与其他老年代的垃圾回收器在触发上没有什么区别
  • 标记是多线程还是单线程?
    • 初始标记是单线程
    • 重新标记是多线程
    • 不太确定,自己去查一下
  • 很多面试官问人,也是看了❓绿皮书???白皮书???觉得你应该会,所以拿过来问你
  • 在重新标记的过程中有可能会产生新的垃圾,也有可能将原来是垃圾的地方变得不再是垃圾,剪掉的线团又捡起粘起来了—->这些垃圾需要重新标记,这个产生是在第二阶段的过程之中,所以需要重新标记
  • 最后进行清理,就相对简单了,但这过程中还会产生浮动垃圾,产生浮动垃圾的时候就没办法了,只能等待下一个阶段再开始的时候在重新来进行清理(初始标记、并发标记、重新标记、并发清理)

CMS的常见问题

  • 内存碎片
    • 因为CMS是mark-sweep的,是标记-清理的算法,所以会产生大量碎片
    • CMS最开始设计出来的时候是对付几百兆的小内存,当内存很大(比如32g)之后用CMS就会出问题
    • 当CMS回收之后会产生大量碎片,这让新生代进入老年代的对象找不到合适的空间了,就会产生PromotionFailed
    • 这时就用Serial Old来代替CMS,让Serial Old用一根线程在这里面做标记-压缩(清除是不行的)
    • ❓CMS设计本意是为了提高响应时间,让响应时间加长????(降低???)
  • 浮动垃圾
    • 最后一个阶段进行并发清理的过程中会产生浮动垃圾—->可以通过下一次的整个阶段来清理
    • 但是在下一个阶段中老年代又满了,那些浮动垃圾又没有清理完,这时该怎么办 或者 老年代中没有足够的空间来分配给新来的对象 (Promotion Failed)
      • 这时应用程序就是暂停并且CMS也会停住
      • 让Serial出来进行垃圾回收
      • 解决方案与内存碎片处理类似,保持老年代有足够空间
    • 实际生产环境中只要日志中有Promotion Failed或者Concurrent Mode Failure,一般就是内存碎片太多了—->导致对象分配不下,这时就换Serial Old来进行垃圾回收
  • CMS本身是为了缩短停顿时间STW才设计出来的,但是不幸的是这两种问题的情况下,一次的停顿时间就会非常长(因为使用了Serial Old)—->一个老太太打扫一个小区
  • 为什么不把SO换成PO????为什么不用多线程—->用单线程时间上是无法忍受的

解决方案

  • CMS基本上避免不了这个问题,因为内存中不可避免的会有内存碎片而且内存又比较大;如果设计好(即编码写得好的话)的话,即在程序中老年代不断地被清理,这样会在一定程度上避免碎片化(可以达到这种程度);假如代码写得跟屎一样,这种碎片化是解决不了的
  • 这也是CMS是一个不太被认可的垃圾回收器,从而不选他作为默认的垃圾回收器
  • 还有一种避免的方法:
    • 降低触发CMS的阈值-XX:CMSInitiatingOccupancyFraction 92%(1.8之前原来是68%,现在1.8改为了92%❓)(1.8之前原来是92%,现在1.8改为了68%,实际上是一个近似值,可能到62%就开始CMS回收了)—->92%指的是整个内存中浮动垃圾问题—->进行FGC的阈值,表示达到98%就进行FGC,可以将这个值降到68%或者50%等等—->以让他有足够的空间进行Promotion,让他有足够的空间产生浮动垃圾;划分出额外的空间给浮动垃圾占据,使其在回收的过程中让他有足够的空间容纳新进入老年代的对象
    • 让空闲空间更大一些,80%之类的
    • 但是调的太小的化也会造成内存浪费很多!!!
    • 当然可能空间再足够,但是程序写得有毛病(不断地在往里面装东西),同样迟早会装满,除非一次FGC会清除很多对象;即便这样,CMS的FGC也会产生那种停顿的问题
    • 参数的默认值想不起来的话就用java -XX:+PrintFlagsFinal去查就行
  • CMS在面对很大的内存时,即便条件不算非常严苛,同样也会产生很长时间的卡顿,因为要进行FGC(不是FGC触发CMS!!!)
  • 硬件升级,系统反而卡顿的问题

image.png
image.png
image.png

FGC与垃圾回收器的关系

  • 当老年代空间不够了,会进行FGC,而FGC是垃圾回收器来执行的;可以设置成PS+PO,也可以设置成PN+CMS(SO)来执行这个FGC
  • 不是FGC触发CMS这个说法,CMS只是垃圾回收器之一
  • 降低阈值只要设计这个参数就行
  • YGC是回收新生代,是新生代的垃圾回收器进行回收
  • 引申出逻辑分代与物理分代的垃圾回收器(G1只逻辑分代,ZGC和Shenandoah逻辑物理都不分代)—->那么他们的垃圾回收是怎样回收的???(这里的重点是算法,因为ZGC和Shenandoah本身就是垃圾回收器!!!)

GC的tuning的目标(三个方面)

  • 调优
  • 解决问题
  • 进行预规划
  • 尽量减少FGC,能没有则没有
  • YGC能解决问题的,FGC就不要上

发展历程

  • CMS在1.4之后就有了
  • G1在1.7才开始有,但是1.7的G1不是特别完善,所以一般说1.8
  • 1.9默认用的就是G1
  • 能说清楚CMS就说,不要说过时了就不说了

垃圾收集器跟内存大小的关系

  • Serial 几十兆
  • PS 上百找到几个G
  • 堆内存越来越大(上几十个G),之前的垃圾回收器怎么调都不行——>调优的过程促进着垃圾回收器的发展
  • CMS 几个G到十几个G(到20G就差不多了)
  • G1 上百G,不光满足内存要求,还满足了停顿时间的要求
  • 上百G乃至上T的内存开始出现并普及
  • ZGC 最大大概是4T
  • 垃圾回收器的升级迭代过程和内存大小的增长是密切相关的
  • 不是说哪一种垃圾回收器是最好的,或者哪一种垃圾回收器的组合是最好的—->要看具体的使用情况!!!每种有每种的特色
    • 老的机器上就一个cpu尽量用Serial,不要搞其他的
    • 其他的要加锁,效率低了,单线程会不需要加锁!!!没准效率就是最高的!
    • 看具体情况,现在大多数情况单线程不靠谱;更多的是PS+PO
  • CMS是一个高响应、低停顿的垃圾回收器,与此带来的问题就是吞吐量降低(碎片化????)