Java高级——垃圾收集器及日志

综述

《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,下图为作用在不同分代的收集器,连线表示可以搭配使用,连线中间的JDK9表示已经取消该组合

在这里插入图片描述
衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency)

新生代收集器

新生代收集器都采用的是标记-复制算法

Serial

单线程工作(指在GC时必须暂停其他线程,直到它收集结束),其简单高效,内存消耗小,适用于内存资源受限的环境,是HotSpot在Client模式下的默认收集器

ParNew

ParNew实际上是Serial的多线程并行版本,除了同时使用多线程进行gc外,其他都一样,在JDK7之前首选的新生代收集器(原因之一是其可以和CMS配合)

Parallel Scavenge

类似ParNew,也是并行收集,其目标是达到一个可控制的吞吐量(运行用户代码时间与处理器消耗时间之比)

  • 停顿时间越短,越适合需要与用户交互或需要保证服务响应质量的程序
  • 吞吐量越高,可高效率地利用CPU资源,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的分析任务

Parallel Scavenge可通过

  • -XX:MaxGCPauseMillis:控制最大gc停顿时间
  • -XX:GCTimeRatio:垃圾收集时间占总时间的比率,相当于吞吐量的倒数,默认为99,即最大允许1%[1/(1+99)]的gc时间
  • -XX:+UseAdaptiveSizePolicy:根据系统运行情况动态调整新生代、老年代等以提供最合适的停顿时间或最大吞吐量

老年代收集器

Serial Old和Parallel Old采用标记-整理,CMS采用标记-清除

Serial Old

Serial Old是Serial老年代版本,同样是单线程,主要也是用于Client模式下的虚拟机

在Serve模式下,与Parallel Scavenge搭配,或作为CMS发生Concurrent Mode Failure时的后背预案

Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,其和Serial Old几乎一样

Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集

CMS

实现了让垃圾收集线程与用户线程同时工作,目标是获取最短回收停顿时间,运作过程分为四个步骤

  • 初始标记:Stop The World,标记GC Roots能直接关联的对象
  • 并发标记:遍历对象引用链,可并发进行
  • 重新标记:Stop The World,修正在并发标记期间产生变动的对象
  • 并发清除:清除删除标记对象,无需移动对象,可并发进行

其缺点有

  • 并发阶段虽然不会Stop The World,但会占用线程导致应用变慢,默认启动的回收线程数为(CPU核心数+3)/4,核心越少影响越大
  • 无法处理浮动垃圾,并发过程产生的垃圾只能在下次收集
  • 不能等老年代快满了再收集,需预留内存(-XX:CMSInitiatingOccupancyFraction)用于程序的并发运行
  • 预留内存不足以分配新对象时,会出现Concurrent Mode Failure,此时需Stop The World,启动Serial Old进行老年代收集

CMS基于标记-清除会导致大量碎片,当内存不足以分配大对象时不得不提前触发Full GC

  • -XX:+UseCMSCompactAtFullCollection:设置Full GC时是否启动内存碎片整理(无法并发),默认开启
  • -XX:CMSFullGCsBeforeCompaction:第n+1次Full GC前会先进行碎片整理(默认为0每次进入Full GC都整理)

全堆收集器G1

在JDK 9 取代Parallel Scavenge+Parallel Old,成为Server模式下的默认收集器,开创了面向局部收集设计思路和基于Region的内存布局形式

  • 将堆分为大小相等的Region(-XX:G1HeapRegionSize)
  • Region可扮演新生代的Eden、Survivor或老年代
  • Humongous用来存储超过Region容量一半的对象,超过整个Region容量的对象放在N个连续的Humongous
  • 用优先级列表记录Region的垃圾价值(即回收所获得的空间大小以及回收所需时间的经验值)
  • 根据信息统计衰减均值,在停顿时间(-XX:MaxGCPauseMillis,默
    认200毫秒)优先处理回收价值收益最大的Region
  • 跨Region引用存储在各自的哈希卡表,key为别的Region起始地址,Value为卡表索引号集合
  • 采用原始快照(SATB)保证并发
  • 用两个TAMS指针记录并发时的内存分配地址,若回收速度赶不上分配速度,需要Stop The World及Full GC

G1的运行过程分为4个步骤:

  • 初始标记:Stop The World(与Minor GC同步),标记GC Roots直接关联的对象,修改TAMS
  • 并发标记:遍历对象引用链,将并发时变动的对象记录到SATB
  • 最终标记:Stop The World,处理SATB
  • 筛选回收:Stop The World,更新Region统计信息,计算价值,根据停顿时间选择回收集,用复制算法回收,不会产生内存碎片

低延迟收集器

可在任意可管理的堆容量下,实现垃圾收集的停顿不超过十毫米

Shenandoah

Shenandoah只包含在OpenJDK中,很多地方都类似与G1,除了以下

  • 支持并发的整理算法
  • 默认不使用分代收集
  • 用连接矩阵代替记忆集,其是二维表格,若Region N有
    对象指向Region M,就在表格的N行M列中打上一个标记

运行过程分为9个步骤:

  • 初始标记:Stop The World,标记GC Roots直接关联的对象
  • 并发标记:遍历对象引用链,将并发时变动的对象记录到SATB
  • 最终标记:Stop The World,处理SATB,选择回收集
  • 并发清理:清理无任何存活对象的Region
  • 并发回收:将回收集存活对象复制到空Region,通过读屏障和Brooks Pointer转发地址解决并发移动
  • 初始引用更新:Stop The World,建立线程集合点,确保对象移动已完成
  • 并发引用更新:将堆中旧地址修改为移动后的新地址,无需搜索引用链,按照地址线性搜索
  • 最终引用更新:Stop The World,修正GC Roots中的引用
  • 并发清理:清理回收集的Region

Brooks Pointer在原有对象结构最前面新增引用字段,在不处于并发移动时指向自己,移动时只需要修改该指针

Brooks Pointer在并发写入可能导致写到旧对象,需采用CAS(Compare And Swap)进行同步

ZGC

基于Region内存布局,不设分代(无跨代引用),使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法

ZGC的Region具有动态创建和销毁,以及动态容量

  • 小型Region:2MB,存放小于256KB的对象
  • 中性Region:32MB,存放大于等256KB但小于4MB的对象
  • 大小Region:动态容量,为2MB的整数倍,存放4MB及以上的对象,只会存放一个,不会重分配

染色指针将信息记录在引用对象的指针上,将64位地址的第19-22位用于存储四个标志信息
在这里插入图片描述
表示其引用对象的三色标记状态、是否进入重分配集(即被移动过)、是否只能通过finalize()访问到,但导致ZGC能够管理的内存不可以超过4TB(242

染色指针的好处有:

  • Region的存活对象被移走后,该Region可立即释放和重用(因为指针自愈),而不必等所有指向该Region的引用都被修正后才能清理
  • 减少内存屏障的使用,将记录对象引用变动的写屏障维护到指针中
  • 可扩展存储其他信息进一步提高性能
  • 实际系统中的地址位是不可随意更改的,需采用多重映射将染色指针转为实际地址

运行分为四个步骤,都可并发,两个阶段中间会存在Stop The World
在这里插入图片描述

  • 并发标记:遍历引用链,更新染色指针中的Marked 0、Marked 1
  • 并发预备重分配:将要清理的Region组成重分配集(需扫描所有Region)
  • 并发重分配:把重分配集的存活对象复制到新Region,为其中的每个Region维护一个转发表,记录旧对象到新对象的转向关系(指针自愈:若出现并发访问,会被截获并转发到新复制的对象,并修正该引用的值)
  • 并发重映射:修正堆中指向重分配集中旧对象的所有引用,并释放重分配表(此步骤合并到并发标记节省一次遍历)

因为ZGC目前不分代,且每次收集扫描所以Region,当收集速度跟不上分配速度时,也会导致错误,解决办法是尽可能增大堆容量(或采用分代)

ZGC支持NUMA(Non-
Uniform Memory Access,非统一内存访问架构),会优先在当前处理器内存上分配对象,避免跨处理器,以保证效率

不收集垃圾——Epsilon

Epsilon不回收垃圾,运行负载极小,适用于只需要短时间运行,在堆耗尽之前退出的应用

收集器的权衡

  • 若是数据分析、科学计算类的任务,目标是能尽快算出结果,则关注吞吐量
  • 若停顿时间直接影响服务质量,严重的甚至会导致事务超时,则关注延迟
  • 还需考虑软硬件、操作系统、CPU、内存、JDK版本及其发行商等

垃圾收集器日志

统一日志框架——Xlog

JDK9之前没有同一框架,日志开关由不同参数决定,JDK 9 后归纳到

-Xlog[:[selector][:[output][:[decorators][:output-options]]]]

selector由Tag和Level组成,可选值为

Tag:gc,add,age,alloc,annotation,aot,arguments,attach,barrier,biasedlocking,blocks,bot,breakpoint,bytecode

Level:Trace,Debug,Info,Warning,Error,Off

decorators可附加日志输出内容,默认为update、level、tags

  • time:当前日期和时间
  • uptime:虚拟机启动到现在经过秒数
  • timemillis:当前时间的毫秒数
  • uptimemillis:虚拟机启动到现在经过的毫秒数
  • timenanos:当前时间的纳秒数
  • uptimenanos:虚拟机启动到现在经过的纳秒数
  • pid:进程ID
  • tid:线程ID
  • level:日志级别
  • tags:日志输出的标签集

基本日志命令(JDK9之前和之后)

查看GC基本信息

-XX: +PrintGC
-Xlog: gc

查看gc详细信息

-XX: +PrintGCDetails
-Xlog: gc*

查看gc前后堆、方法区可用容量变化

-XX: +PrintHeapAtGC
-Xlog: gc+heap=debug

查看gc过程中用户线程并发时间以及停顿的时间

-XX: +PrintGCApplicationConcurrentTime -XX: +PrintGCApplicationStoppedTime
-Xlog: safepoint

查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收集器开始支持)自动调节的相关信息

-XX:+PrintAdaptiveSizePolicy
-Xlog:gc+ergo*=trace

查看熬过收集后剩余对象的年龄分布信息

-XX:+PrintTenuringDistribution
-Xlog:gc+age=trace

其他JDK9前后命令
在这里插入图片描述
在这里插入图片描述

其他

在这里插入图片描述
在这里插入图片描述


版权声明:本文为qq_35258036原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。