从零开始JVM

ParNew回收器(复制整理)

从零开始JVM - 图1

到底是用单线程垃圾回收好,还是多线程垃圾回收好?到底是Serial垃圾回收器好还是ParNew垃圾回收器好?

  • 多核CPU服务器或者大型系统,垃圾回收,用ParNew更好,因为可以充分利用CPU资源,提升性能
  • 单核CPU操作系统或者客户端程序,垃圾回收,用Serial更好,因为单CPU运行多线程会频繁的上下文切换,加重了性能开销

一个面试题,parnew+cms的gc,如何保证只做ygc,jvm参数如何配置?

  1. 加大分代年龄,比如默认15加到30;
  2. 修改新生代老年代比例,比如新生代老年代比例改成2:1
  3. 修改e区和s区比例,比如改成6:2:2
  • 结合案例和画图给面试官说明,而不是干巴巴的简单给几个方法,可以回答的有血有肉,让面试官无话可说。
  • 根据他的内存占用情况,GC后的对象存活情况,合理分配Eden、Survivor、老年代的内存大小,合理设置一些参数,即可做到。

CMS回收器(标记清理)

标记-清理算法

其实就是我们之前给大家讲过的一个算法,先通过追踪GC Roots的方法,看
看各个对象是否被GC Roots给引用了,如果是的话,那就是存活对象,否则就是垃圾对象

CMS在执行一次垃圾回收的过程一共分为4个阶段:

  1. 初始标记
    • 系统Stop the World
    • 标记出来所有GC Roots直接引用的对象
  2. 并发标记
    • 系统可以创建对象(存活or垃圾)
    • 对已有对象进行 GC Roots追踪,标记存活or垃圾。
    • 对老年代所有的对象进行GC Roots 追踪是最费时的
  3. 重新标记
    • 之前没标记的存活和垃圾对象
    • 系统Stop the World ,重新标记
  4. 并发清理
    • 回收需要清理的对象 (耗时)

CMS垃圾回收的触发时机,其中有一个就是当老年代内存占用达到一定比例了,就自动执行GC。

Concurrent Mode Failure

  • 如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间
  • 这个时候会发生Concurrent Mode Failure,就是说并发垃圾回收失败了,内存不够
  • 此时就会自动用“Serial Old”垃圾回收器替代CMS,就是直接强行把系统程序“Stop the World”
  • 重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程

内存碎片问题

CMS不是完全就仅仅用“标记-清理”算法的,因为太多的内存碎片实际上会导致更加频繁的Full GC。

CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开了
他意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片
连续内存空间,避免内存碎片。

还有一个参数是“-XX:CMSFullGCsBeforeCompaction”,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认
是0,意思就是每次Full GC之后都会进行一次内存整理。

为啥老年代的Full GC要比新生代的Minor GC慢很多倍,一般在10倍以上?

新生代执行速度其实很快,因为直接从GC Roots出发就追踪哪些对象是活的就行了,新生代存活对象是很少的,这个速度是极快的,
不需要追踪多少对象。

然后直接把存活对象放入Survivor中,就一次性直接回收Eden和之前使用的Survivor了。

但是CMS的Full GC呢?

在并发标记阶段,他需要去追踪所有存活对象,老年代存活对象很多,这个过程就会很慢;

其次并发清理阶段,他不是一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象,速度也很慢;

最后完事儿了,还得执行一次内存碎片整理,把大量的存活对象给挪在一起,空出来连续内存空间,这个过程还得“Stop the
World”,那就更慢了。

万一并发清理期间,剩余内存空间不足以存放要进入老年代的对象了,引发了“Concurrent Mode Failure”问题,那更是麻烦,还得
立马用“Serial Old”垃圾回收器,“Stop the World”之后慢慢重新来一遍回收的过程,这更是耗时了。

所以综上所述,老年代的垃圾回收,就是一个字:慢

几个触发老年代GC的时机吗?

  1. 老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,一般空间担保参数都会打
    开;
  2. 老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
  3. 新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足。
  4. 今天加了一个触发时机,就是“-XX:CMSInitiatingOccupancyFaction”参数
  5. 刨除掉上述几种情况,如果老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但是老年代已经使用的
    内存空间超过了这个参数指定的比例,也会自动触发Full GC

G1回收器

G1垃圾回收器的设计思想,包括Region的划分,然后Region动态转移给新生代或者老年代,按需分配

从零开始JVM - 图2

G1最大的一个特点,就是可以让我们==设置一个垃圾回收的预期停顿时间==

也就是说比如我们可以指定:希望G1同志在垃圾回收的时候,可以保证,在1小时内由G1垃圾回收导致的“Stop the World”时间,
也就是系统停顿的时间,不能超过1分钟。

G1是如何做到对垃圾回收导致的系统停顿可控的?

其实G1如果要做到这一点,他就必须要追踪每个Region里的回收价值,啥叫做回收价值呢?

他必须搞清楚每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾?

G1 回收过程

  1. 初始标记
    • Stop the World 标记一下GC Roots直接能引用的对象
    • 这个过程速度是很快的
  2. 并发标记
    • 允许系统程序的运行
    • 同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象
    • 并发标记阶段还是很耗时的,因为要追踪全部的存活对象
    • 这个阶段是可以跟系统程序并发运行的,所以对系统程序的影响不太大
  3. 最终标记阶段
    • Stop the World
    • 根据并发标记 阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象
  4. 混合回收
    • 这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回
      收的预期性能和效率
    • 会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我
      们指定的范围内