一:G1

概念 Garbage-First 面向服务器的垃圾收集器

针对配置多颗处理器,大内存的机器,满足GC停顿,高吞吐的特征
逻辑上分代,物理上分区块
Region:独立的区,不超过2048个区,JVM源码TAGET_REGION_NUMBER规定,可以超,但不建议
区块大小,堆/2048,一般2M,参数:-XX:G1HeapRegionSize指定
eden: 5%占比,-XX:G1NewSizePercent 设置
系统会自动调整占比,最多60%,-XX:G1MaxNewSizePercent设置
e区S区比例默认8:1:1
Region的区域是动态变化的
Humongous:大文件区:超过50%就存进去,可以跨多个区
FullGC 也会将H区回收

过程

image.png
一次gc mixed GC 大致过程

  1. 初始标记(initial mark ):STW,记录gc roots直接引用的对象,速读很快
  2. 并发标记(concurrent marking):同CMS并发标记
  3. 最终标记(Remark):STW,同CMS重新标记
  4. 筛选回收(CleanUP) :STW,region的回收价值和成本排序,,根据所期望的STW,制定回收计划,不一定

全部回收。
-XX:MaxGCPauseMillis制定期望STW时间:
一定要切合实际,不能乱来,默认200ms
Collection Set:需要回收的region 集合
复制算法,region区复制到另一个region区,不会有碎片,不需要整理
回收是用户线程并发
Collection Set:回收价值排序的region列表,有优先级别的,最少的时间,最多的内存

G1垃圾回收器的分类

Young GC
并不是所有eden区放满都会释放,会计算需要的时间,如果远小于,会继续增加年轻region,
计算回收时间接近-XX:MaxGCPauseMills 就会触发YoungGC
MixedGC
老年代堆的占用率达到:-XX:InitiationgHeapOccupancyPercent 时触发
回收 所有Young 和部分的 Old(有优先顺序的那种),大对象区
G1 先做MixedGC, 复制算法,如果没有足够的空闲Region 会触发Full GC
Full GC
停止系统程序,单线程标记,清理,压缩整理,非常耗时

jvm参数

-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),
默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),
Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)
总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold:最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),
则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆
默认有2048个region,如果有接近1000个region都是老年代的region,
则可能就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于
这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),
在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,
一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,
在混合回收的时候,对Region回收都是基于复制算法进行的,
都是把要回收的Region里的存活对象放入其他Region,
然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,
一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,
意味着本次混合回收就结束了。

  1. -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+G1NewSizePercent=50
  2. -Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
  3. -XX:+PrintGCTimeStamps -XX:+PrintGCCause

修改g1JVM参数要加 :-XX:+UnlockExperimentalVMOptions

orcale 官方文档
https://www.oracle.com/cn/technical-resources/articles/java/g1gc.html

优化和建议

假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,
年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。
那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,
就会进入老年代中。
或者是你年轻代gc过后,存活下来的对象过多,
导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,
也会快速导致一些对象进入老年代中。
所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,
在保证他的年轻代gc别太频繁的同时,
还得考虑每次gc过后的存活对象有多少,
避免存活对象太多快速进入老年代,频繁触发mixed gc.

什么场景适合使用G1

  1. 50%以上的堆被存活对象占用
  2. 对象分配和晋升的速度变化非常大
  3. 垃圾回收时间特别长,超过1秒
  4. 8GB以上的堆内存(建议值)
  5. 停顿时间是500ms以内

每秒几十万并发的系统如何优化JVM
Kafka类似的支撑高并发消息系统大家肯定不陌生,对于kafka来说,每秒处理几万甚至几十万消息时很正常的,一般来说部署kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就涉及到一个问题了,我们以前常说的对于eden区的young gc是很快的,这种情况下它的执行还会很快吗?很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按kafka这个并发量放满三四十G的eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为young gc卡顿几秒钟没法处理新消息,显然是不行的。那么对于这种情况如何优化了,我们可以使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。
G1天生就适合这种大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题。

二:ZGC

下图对应支持的JDK版本
image.png

概念

ZGC的目标

  1. 支持TB级别的堆
  2. 最大STW不超过10ms
  3. 奠定未来GC特性基础
  4. 最糟糕情况吞吐量降低15%

不分代(暂时)

ZGC 内存布局

基于Region内存布局,暂时不分代,使用读屏障,颜色指针,并发的标记-整理算法,低延时首要目的
如图所示内存三个区

  1. 小型区 small Region : 固定2M , 放小于256k的对象
  2. 中型 Medium:固定32M,放256k~4M的对象
  3. 大型Large:不固定大小,可动态变化,但必须是2M的倍数,每个大型区只放一个大对象

可能比中型小,不会被重分配,
image.png

NUMA_aware
UMA:Uniform Memory Access Architecture:只有一块内存,所有cpu 都去访问,竞争激烈
NUMA : Non Uniform Memory Access Architecture,每个CPU 对应一块内存,且最近,效率高
ZGC 会自动感知NUMA架构和充分利用NUMA结构特性
image.png

过程

  1. 并发标记Concurrent Mark:同G1 ,遍历对象图,可达性分析,初始标记,
    最终标记会STW 很短暂,标记不在对象上,而是在区域上,颜色指针标记 MarkEd 0 ,Marked 1 标志位
  2. 并发预备重分配Concurrent Prepare for Relocat:查询本次收集过程需要清理哪些Region, 再把Region
    重组分配集 Relocation Set,会扫描所有region,
  3. 并发重分配Concurrent Relocate:核心阶段,重分配集中存活的对象复制到新的Region,并为重分配表中
    中的每个Region 维护一个转发表 Forward Table,记录从旧对象到新对象的转向关系
    ZGC收集器能从引用上得知一个对象是否处于重分配集之中,如果用户线程并发访问了重分配集中的对象,
    这次访问会被配置的内存屏障截获,然后根据转发表上的记录,转发到新复制的对象上,
    并修改更新该引用的值,:这种行为:指针的自愈能力,
  4. 并发重映射:修正整个堆中指向重分配集中的所有对象。重映射并不迫切,可以合并到下一次GC
    的并发标记中

image.png
颜色指针:
Colored Pointers,颜色指针ZGC的GC信息保存在指针中
image.png
每个对象有一个64位指针,这64位被分为:

  • 18位:预留给以后使用;
  • 1位:Finalizable标识,与并发引用处理有关,它表示这个对象只能通过finalizer才能访问;
  • 1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的Region集合);
  • 1位:Marked1标识;
  • 1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC;
  • 42位:对象的地址(所以它可以支持2^42=4T内存)

两个标识位:?重新标记在下次GC
每个周期开始,交换使用的标记位,如上次GC周期袖中已失效,变为未标记
GC周期1:使用mark0, 周期结束标为01
GC周期2:使用mark1,期待的mark标记10,所有引用重新标记
颜色指针的三大优势:

  1. 一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。
  2. 颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。
  3. 颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能

    JVM参数

    JVM参数即可:-XX:+UnlockExperimentalVMOptions 「-XX:+UseZGC」
    image.png

优化建议

三:安全点&安全区域

也叫安全点,如GC的时候必须要等到Java线程都进入到safepoint的时候VMThread才能开始执行GC

  1. 循环的末尾 (防止大循环的时候一直不进入safepoint,而其他线程在等待它进入safepoint)
  2. 方法返回前
  3. 调用方法的call之后
  4. 抛出异常的位置