0%

本文参数基于JDK 8整理

收集器 参数及默认值 备注
Serial -XX:+UseSerialGC 虚拟机在Client模式下的默认值,开启后,使用 Serial + Serial Old 的组合
ParNew -XX:+UseParNewGC 开启后,使用ParNew + Serial Old的组合
-XX:ParallelGCThreads=n 设置垃圾收集器在并行阶段使用的垃圾收集线程数,当逻辑处理器数量小于8时,n的值与逻辑处理器数量相同;如果逻辑处理器数量大于8个,则n的值大约为逻辑处理器数量的5/8,大多数情况下是这样,除了较大的SPARC系统,其中n的值约为逻辑处理器的5/16。
Parallel Scavenge -XX:+UseParallelGC 虚拟机在Server模式下的默认值,开启后,使用 Parallel Scavenge + Serial Old的组合
-XX:MaxGCPauseMillis=n 收集器尽可能保证单次内存回收停顿的时间不超过这个值,但是并不保证不超过该值
-XX:GCTimeRatio=n 设置吞吐量的大小,取值范围0-100,假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集
-XX:+UseAdaptiveSizePolicy 开启后,无需人工指定新生代的大小(-Xmn)、 Eden和Survisor的比例(-XX:SurvivorRatio)以及晋升老年代对象的年龄(-XX:PretenureSizeThreshold)等参数,收集器会根据当前系统的运行情况自动调整
Serial Old Serial Old是Serial的老年代版本,主要用于 Client 模式下的老生代收集,同时也是 CMS 在发生 Concurrent Mode Failure时的后备方案
Parallel Old -XX:+UseParallelOldGC 开启后,使用Parallel Scavenge + Parallel Old的组合。Parallel Old是Parallel Scavenge的老年代版本,在注重吞吐量和 CPU 资源敏感的场合,可以优先考虑这个组合
CMS -XX:+UseConcMarkSweepGC 开启后,使用ParNew + CMS的组合;Serial Old收集器将作为CMS收集器出现 Concurrent Mode Failure 失败后的后备收集器使用
-XX:CMSInitiatingOccupancyFraction=68 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认68%
-XX:+UseCMSCompactAtFullCollection 在完成垃圾收集后是否要进行一次内存碎片整理,默认开启
-XX:CMSFullGCsBeforeCompaction=0 在进行若干次Full GC后就进行一次内存碎片整理,默认0
-XX:+UseCMSInitiatingOccupancyOnly 允许使用占用值作为启动CMS收集器的唯一标准,一般和CMSFullGCsBeforeCompaction配合使用。如果开启,那么当CMSFullGCsBeforeCompaction达到阈值就开始GC,如果关闭,那么JVM仅在第一次使用CMSFullGCsBeforeCompaction的值,后续则自动调整,默认关闭。
-XX:+CMSParallelRemarkEnabled 重新标记阶段并行执行,使用此参数可降低标记停顿,默认打开(仅适用于ParNewGC)
-XX:+CMSScavengeBeforeRemark 开启或关闭在CMS重新标记阶段之前的清除(YGC)尝试。新生代里一部分对象会作为GC Roots,让CMS在重新标记之前,做一次YGC,而YGC能够回收掉新生代里大多数对象,这样就可以减少GC Roots的开销。因此,打开此开关,可在一定程度上降低CMS重新标记阶段的扫描时间,当然,开启此开关后,YGC也会消耗一些时间。PS. 开启此开关并不保证在标记阶段前一定会进行清除操作,生产环境建议开启,默认关闭。
CMS-Precleaning -XX:+CMSPrecleaningEnabled 是否启用并发预清理,默认开启
CMS-AbortablePreclean -XX:CMSScheduleRemark EdenSizeThreshold=2M 如果伊甸园的内存使用超过该值,才可能进入“并发可中止的预清理”这个阶段
CMS-AbortablePreclean -XX:CMSMaxAbortablePrecleanLoops=0 “并发可终止的预清理阶段”的循环次数,默认0,表示不做限制
CMS-AbortablePreclean -XX:+CMSMaxAbortablePrecleanTime=5000 “并发可终止的预清理”阶段持续的最大时间
-XX:+CMSClassUnloadingEnabled 使用CMS时,是否启用类卸载,默认开启
-XX:+ExplicitGCInvokesConcurrent 显示调用System.gc()会触发Full GC,会有Stop The World,开启此参数后,可让System.gc()触发的垃圾回收变成一次普通的CMS GC。
-XX:+UseG1GC 使用G1收集器
-XX:G1HeapRegionSize=n 设置每个region的大小,该值为2的幂,范围为1MB到32MB,如不指定G1会根据堆的大小自动决定
-XX:MaxGCPauseMillis=200 设置最大停顿时间,默认值为200毫秒。
-XX:G1NewSizePercent=5 设置年轻代占整个堆的最小百分比,默认值是5,这是个实验参数。需用-XX:+UnlockExperimentalVMOptions解锁试验参数后,才能使用该参数。
-XX:G1MaxNewSizePercent=60 设置年轻代占整个堆的最大百分比,默认值是60,这是个实验参数。需用-XX:+UnlockExperimentalVMOptions解锁试验参数后,才能使用该参数。
-XX:ParallelGCThreads=n 设置垃圾收集器在并行阶段使用的垃圾收集线程数,当逻辑处理器数量小于8时,n的值与逻辑处理器数量相同;如果逻辑处理器数量大于8个,则n的值大约为逻辑处理器数量的5/8,大多数情况下是这样,除了较大的SPARC系统,其中n的值约为逻辑处理器的5/16。
-XX:ConcGCThreads=n 设置垃圾收集器并发阶段使用的线程数量,设置n大约为ParallelGCThreads的1/4。
-XX:InitiatingHeapOccupancyPercent=45 老年代大小达到该阈值,就触发Mixed GC,默认值为45。
-XX:G1MixedGCLiveThresholdPercent=85 Region中的对象,活跃度低于该阈值,才可能被包含在Mixed GC收集周期中,默认值为85,这是个实验参数。需用-XX:+UnlockExperimentalVMOptions解锁试验参数后,才能使用该参数。
-XX:G1HeapWastePercent=5 设置浪费的堆内存百分比,当可回收百分比小于浪费百分比时,JVM就不会启动Mixed GC,从而避免昂贵的GC开销。此参数相当于用来设置允许垃圾对象占用内存的最大百分比。
-XX:G1MixedGCCountTarget=8 设置在标记周期完成之后,最多执行多少次Mixed GC,默认值为8。
-XX:G1OldCSetRegionThresholdPercent=10 设置在一次Mixed GC中被收集的老年代的比例上限,默认值是Java堆的10%,这是个实验参数。需用-XX:+UnlockExperimentalVMOptions解锁试验参数后,才能使用该参数。
-XX:G1ReservePercent=10 设置预留空闲内存百分比,虚拟机会保证Java堆有这么多空间可用,从而防止对象晋升时无空间可用而失败,默认值为Java堆的10%。
-XX:-G1PrintHeapRegions 输出Region被分配和回收的信息,默认false
-XX:-G1PrintRegionLivenessInfo 在清理阶段的并发标记环节,输出堆中的所有Regions的活跃度信息,默认false
Shenandoah -XX:+UseShenandoahGC 使用UseShenandoahGC,这是个实验参数,需用-XX:+UnlockExperimentalVMOptions解锁试验参数后,才能使用该参数;另外该参数只能在Open JDK中使用,Oracle JDK无法使用
ZGC -XX:+UseZGC 使用ZGC,这是个实验参数,需用-XX:+UnlockExperimentalVMOptions解锁试验参数后,才能使用该参数;
Epsilon -XX:+UseEpsilonGC 使用EpsilonGC,这是个实验参数,需用-XX:+UnlockExperimentalVMOptions解锁试验参数后,才能使用该参数;

引用:

Getting Started with the G1 Garbage Collector

JVM各个发行版的JVM参数

其他垃圾收集器

Shenandoah

厂商:RedHat,贡献给了OpenJDK

定位:低延迟垃圾收集器

状态:实验性

限制:Oracle JDK无法使用

适用版本:详见 https://wiki.openjdk.java.net/display/shenandoah

网址:https://wiki.openjdk.java.net/display/shenandoah

和G1对比,相同点:

  • 基于Region的内存布局
  • 有用于存放大对象的Humongous Region
  • 回收策略也同样是优先处理回收价值最大的Region

和G1对比,不同点:

  • 并发的整理算法
  • Shenandoah默认是不使用分代收集的
  • 解决跨region引用的机制不同,G1主要基于Rememberd Set、CardTable,而Shenandoah是基于连接矩阵(Connection Matrix)去实现的。

启用参数:

1
-XX:+UnlockExperimentalVMOptions  -XX:+UseShenandoahGC

适用场景:

  • 低延迟、响应快的业务场景

工作步骤:

相关论文:https://www.researchgate.net/publication/306112816_Shenandoah_An_open-source_concurrent_compacting_garbage_collector_for_OpenJDK

  1. 初始标记(Initial Marking):

    与G1一样,首先标记与GC Roots直接关联的对象,存在Stop The World

  2. 并发标记(Concurrent Marking)

    与G1一样,标记出全部可达的对象,该阶段并发执行,无Stop The World

  3. 最终标记(Final Marking)

    统计出回收价值最高的Region、构建回收集(Collection Set)。存在Stop The World

  4. 并发清理(Concurrent Cleanup)

    用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)

  5. 并发回收(Concurrent Evacuation)

    并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决。并发回收阶段运行的时间长短取决于回收集的大小。

  6. 初始引用更新(Initial Update Reference)

    并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的停顿。

  7. 并发引用更新(Concurrent Update Reference)

    真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。主要是按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。

  8. 最终引用更新(Final Update Reference)

    解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关

  9. 并发清理(Concurrent Cleanup)

    经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用

TIPS

步骤较多,重点是:并发标记、并发回收、并发引用更新这三个阶段。

性能表现

图片描述

ZGC

厂商:Oracle

定位:低延迟垃圾收集器

状态:实验性

限制:JDK14之前,无法在Windows、macOS机器上使用,每个版本的特性解介绍详见:https://wiki.openjdk.java.net/display/zgc/Main

核心技术:染色指针技术

适用场景:

  • 低延迟、响应快的业务场景

启用参数:

1
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

内存布局:

  • ZGC也采用基于Region的堆内存布局,但与它们不同的是,ZGC的Region(在一些官方资料中将它称为Page或者ZPage,本章为行文一致继续称为Region)具有动态性,可以动态创建和销毁,以及动态的区域容量大小。region的容量:
    • 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象
    • 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象
    • 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到)的,因为复制一个大对象的代价非常高昂

工作步骤:

  • 并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。
  • 并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。重分配集与G1收集器的回收集(Collection Set)还是有区别的,ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。
  • 并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。
  • 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。

性能表现:

  • 吞吐量:
    图片描述

  • 停顿时间:
    图片描述

Epsilon

Epsilon(A No-Op Garbage Collector)垃圾回收器控制内存分配,但是不执行任何垃圾回收工作。一旦java的堆被耗尽,jvm就直接关闭。设计的目的是提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低消费内存占用量和内存吞吐时的延迟时间。一个好的实现是隔离代码变化,不影响其他GC,最小限度的改变其他的JVM代码。

定位:不干活儿的垃圾收集器

状态:实验性

启用参数:

1
-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC

适用场景:

  • Performance testing,什么都不执行的GC非常适合用于差异性分析。no-op GC可以用于过滤掉GC诱发的新能损耗,比如GC线程的调度,GC屏障的消耗,GC周期的不合适触发,内存位置变化等。此外有些延迟者不是由于GC引起的,比如scheduling hiccups, compiler transition hiccups,所以去除GC引发的延迟有助于统计这些延迟。
  • Memory pressure testing, 在测试java代码时,确定分配内存的阈值有助于设置内存压力常量值。这时no-op就很有用,它可以简单地接受一个分配的内存分配上限,当内存超限时就失败。例如:测试需要分配小于1G的内存,就使用-Xmx1g参数来配置no-op GC,然后当内存耗尽的时候就直接crash。
  • VM interface testing, 以VM开发视角,有一个简单的GC实现,有助于理解VM-GC的最小接口实现。它也用于证明VM-GC接口的健全性。
  • Extremely short lived jobs, 一个短声明周期的工作可能会依赖快速退出来释放资源,这个时候接收GC周期来清理heap其实是在浪费时间,因为heap会在退出时清理。并且GC周期可能会占用一会时间,因为它依赖heap上的数据量。
  • Last-drop latency improvements, 对那些极端延迟敏感的应用,开发者十分清楚内存占用,或者是几乎没有垃圾回收的应用,此时耗时较长的GC周期将会是一件坏事。
  • Last-drop throughput improvements, 即便对那些无需内存分配的工作,选择一个GC意味着选择了一系列的GC屏障,所有的OpenJDK GC都是分代的,所以他们至少会有一个写屏障。避免这些屏障可以带来一点点的吞吐量提升。

参考文档

[TOC]

引言

下面这张图是 Java 中比较主流的基于分代收集理论的垃圾收集器,以及它们能够作用的JVM内存区域。

image-20220311164628901

术语

  • STW:全局停顿,Java 代码停止运行,native 代码继续运行,但不能与 JVM 进行交互。

    • STW 原因:多半由于垃圾回收导致;也可能是由 Dump 线程、死锁检查和 Dump 堆等导致的。

    • STW 危害:服务停止、毫无响应;主从切换、危害生产环境。

  • 并行收集:指多个垃圾回收线程并行工作,但是收集的过程中,用户线程还是处于等待状态。

  • 并发收集:指用户线程与垃圾收集线程同时工作。

  • 吞吐量:CPU 用于运行用户代码的时间与 CPU 总消耗时间的对比

    • 公式:运行用户代码时间/(运行用户代码时间+垃圾收集时间)

垃圾收集器介绍

Serial 收集器(新生代)

image-20220311171643510

  • 最基本的、历史最悠久的收集器
  • 算法:复制算法
  • 特点:
    • 简单、高效
    • 单线程
    • 垃圾回收过程中 STW

ParNew收集器(新生代)

image-20220311173051813

  • Serial 收集器的多线程版,除了使用多线程外,其它的和Serial收集器一样。

  • 特点:

    • 多线程
    • 可以设置垃圾收集的线程数(-XX:ParallelGCThreads)
  • 使用场景:主要用来和 CMS 收集器配合使用

Parallel Scavenge收集器(新生代)

image-20220314152651631

  • 关注的是吞吐量,也叫吞吐量收集器
  • 采用的也是复制算法
  • 也是并行的多线程收集器,这一点和 ParNew 类似
  • 特点:
    • 可以达到一个可控制的吞吐量,有两个 JVM 参数可以配置:
      • -XX:MaxGCPauseMillis:控制最大的垃圾收集停顿时间(尽力)
      • -XX:GCTimeRatio:设置吞吐量的大小,取值 0-100, 系统花费不超过 1/(1+n) 的时间用于垃圾收集
    • 自适应 GC 策略:可用 -XX:+UseAdptiveSizePolicy 打开
      • 打开自适应策略后,无需手动设置新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)等参数
      • 虚拟机会自动根据系统的运行状况收集性能情况,动态的调整这些参数,从而达到最优的停顿时间以及最高的的吞吐量
  • 使用场景:注重吞吐量的场景

Serial Old 收集器(老年代)

image-20220314161053707

  • Serial收集器的老年代
  • 算法:标记-整理
  • 使用场景:
    • 可以和 Serial、ParNew、Parallel Scavenge 这三个新生代的垃圾收集器配合使用
    • CMS 收集器出现故障的时候,会用 Serial Old 作为后备

Parallel Old 收集器(老年代)

image-20220314161516210

  • Parallel Scavenge 收集器的老年代版本
  • 算法:标记整理
  • 特点:只能和 Parallel Scavenge 配合使用
  • 使用场景:关注吞吐量的场景

CMS 收集器(老年代)

全称叫做 Concurrent Mark Sweep

image-20220314162206027

  • 并发收集器

  • 算法:标记-清除

  • CMS 收集器执行过程

    • 初始标记(initial mark)
      • 标记 GC Roots 能直接关联到的对象
      • Stop The World
    • 并发标记(concurrent mark)
      • 找出所有 GC Roots 能关联到的对象
      • 并发执行,无 Stop The World
    • 并发预清理(concurrent-preclean)
      • 重新标记那些在并发标记阶段,引用被更新的对象,从而减少后面重新标记阶段的工作量
      • 并发执行,无 Stop The World
      • 可用 -XX:-CMSPrecleaningEnabled 关闭并发预发清理阶段,默认打开。
    • 并发可中止的预清理阶段(concurrent-abort-oreclan)
      • 和并发预清理做的事一样,并发执行,无 StopTheWorld。
      • 当 Eden 的使用量大于 CMSScheduleRemarkEdenSizeThreashold 的阈值(默认 2M)时,才会执行该阶段
      • 主要作用:允许我们能够控制预清理阶段的结束时机。比如扫描多长时间(CMSMaxAbortablePrecleanTime, 默认 5 秒)或者 Eden 区使用占比打到一定阈值(CMSScheduleRemarkEdenPenetration,默认 50%)就结束本阶段
    • 重新标记
      • 修正并发标记期间,因为用户程序继续运行,导致标记发生变动的那些对象的标记
      • 一般来说,重新标记花费的时间会比初始标记阶段长一点,但比并发标记的时间短
      • 存在 Stop The World
    • 并发清除
      • 基于标记结果,清除掉要清除前面标记出来的垃圾
      • 并发执行,无 Stop The World
    • 并发重置
      • 清理本次 CMS GC 的上下文信息,为下一次 GC 做准备
  • 优点:

    • Stop The World 的时间比较短,只有初始标记和重新标记阶段存在 Stop The World,其它阶段都是并发执行的
    • 大多数的过程都是并发执行的
  • 缺点:

    • CPU 资源比较敏感:并发阶段可能导致应用吞吐量的降低

    • 无法处理浮动垃圾(并发清除阶段时,用户线程生产出来的垃圾,无法在本次收集时间内处理)

    • 不能等老年代几乎满了才开始收集

      • 预留的内存不够 -> Concurrent Mode Failure -> Serial Old 作为后备
      • CMSInitiatingOccupancyFraction 设置老年代占比达到多少就触发垃圾收集,默认 68%
    • 内存碎片

      • 标记-清除导致碎片的产生
      • UseCMSCompactAtFullCollection:在完成 Full GC 后是否要进行内存碎片整理,默认开启
      • CMSFullGCsBeforeCompaction:进行几次 Full GC 后就进行一次内存碎片整理,默认 0
  • 使用场景:

    • 希望系统停顿时间短,响应速度快的场景,比如各种应用程序

G1 收集器

  • Garbge First

  • 面向服务端应用的垃圾收集器

  • 内存布局
    image-20220314171405933

    • 将整个JVM分成若干个大小相等的区域,每个区域叫做 Region
    • 每个 Region的大小可通过 -XX:G1HeapRegionSize 指定 Region 的大小
    • Region 取值范围为 1M~32M,应为 2 的 N 次幂
    • Region 的分类:Eden、survivor、Old、Humongous
    • 在 G1 里面,同一个代里面的对象可能是不连续的
    • Humongous 是用来存储大对象的,某个对象超过了 Region的一半就认为是大对象,如果对象超级大,就放在多个联系的 humongous 里面
    • G1 会将 Humongous 也看作老年代来处理
  • 设计思想

    • 若干个 Region
    • 跟踪每个 Region 里面的垃圾堆积的价值大小
    • G1 在后台构建一个优先列表,根据允许的收集时间,优先回收价值高的 Region,这样就可以获得更高的回收效率
  • 垃圾收集机制

    • Young GC

      • 当所有的Eden都满了的时候,就会触发 Young GC,
      • 所有的 Eden 里面的对象会转移到 Survivor Region 里面去,
      • 而原先 Survivor Region 里面的对象转移到新的Survivor Region中,或者晋升到 Old Region,
      • 最后,空闲Region会被放入空闲列表中,等待下次被使用
    • Mixed GC

      • 当老年代大小占整个堆的百分比达到一定阈值(可用-XX:InitiatingHeapOccupancyPercent指定,默认 45%),就触发 Mixed GC,

      • Mixed GC 会回收所有 Young Region,同时回收部分 Old Region,

      • Mixed GC 执行过程

        image-20220316150715171

        • 初始标记,标记 GC Roots 能直接关联到的对象,和 CMS 类似,存在 Stop The World
        • 并发标记,同 CMS 的并发标记,并发执行,没有 Stop The World
        • 最终标记,修正在并发标记期间引起的变动,存在 Stop The World
        • 筛选回收,对各个Region的回收价值和成本进行排序,根据用户所期望的的停顿时间(根据MaxGCPauseMillis来指定)来制定回收计划,并选择一些 Region 回收,回收过程如下:
          • 选择一系列 Region 构成一个回收集
          • 把决定回收的 Region 中的存活对象复制到空的 Region 中
          • 删掉需回收的Region(无内存碎片)
    • Full GC

      • 复制对象的内存不够,或者无法分配足够的内存(比如,巨型对象没有足够的连续分区分配)时,会触发Full GC,
      • Full GC 模式下,使用 Serial Old 模式,
      • 因此 G1 的优化原则就是尽量减少 Full GC 的发生。
      • 那么,如何减少Full GC呢?
        • 增加预留内存(增大 -XX:G1ReservePercent,默认为堆的 10%);
        • 更早地回收垃圾(减少 -XX:InitatingHeapOccupancyPercent,老年代达到该阈值就会触发 Mixed GC,默认 45%);
        • 增加并发阶段使用的线程数(增大 -XX:ConcGCThreads)
  • 特点:

    • 可以作用在整个堆、
    • 可控的停顿(MaxGCPauseMillis=200)、
    • 无内存碎片
  • 使用场景:

    • 占用内存较大的应用(6G 以上)
    • 替换 CMS 垃圾收集器

G1 收集器 VS CMS收集器

对于JDK 8:都可以用

  • 如果内存小于等于 6G,建议用 CMS,如果内存大于6G,考虑使用 G1。

如果 JDK 版本大于 8 以上,则用 G1, 因为从 JDK9 开始,CMS已被废弃了。

链接

  1. 分代收集理论

    本文所讲述的垃圾收集器都是基于分代收集理论的

  2. JVM内存结构简单分析

    垃圾收集器回收的都有哪些JVM内存区域,这些内存区域在JVM中是怎么分布的?分别有哪些数据?

  3. JVM-垃圾回收算法

    本文提到的垃圾收集器所使用的垃圾回收算法的原理

  4. JVM-垃圾回收

    提到的 GC Roots 是怎么回事

0 概述

基础垃圾算法:标记-清除、标记-整理、复制

综合垃圾回收算法:分代收集算法、增量算法

1 标记-清除(Mark-Sweep)

  1. 标记需要回收的对象

  2. 清理掉要回收的对象

缺点:垃圾回收之后会存在内存碎片

2 标记-整理(Mark-Compact)

  1. 标记需要回收的对象

  2. 把所有的存活对象压缩的内存的一端

  3. 清理掉边界外的所有空间

这个算法可以避免标记-清除算法会产生的内存碎片

3 复制(Copy)

image-20220303223424546

  1. 把内存分为两块,每次只使用一块

  2. 把当前使用的内存中的存活对象复制到未使用的内存中去,然后清除掉正在使用的内存中的所有对象

  3. 交换两个内存的角色,等待下次回收

4 三种算法对比

回收算法 有点 缺点
标记-清除 实现简单 存在内存碎片、分配内存速度会受影响
标记-整理 无碎片 整理内存存在开销
复制 性能好、无碎片 内存利用率低

5 分代收集算法

  1. 各种商业虚拟机堆内存的垃圾回收基本上都采用了垃圾回收

  2. 根据对象的存活周期,把内存分成多个区域,不同区域使用不同的回收算法回收对象

    image-20220311113729890

5.1 回收类型

  • 新生代回收(Minor GC|Young GC)
  • 老年代回收(Major GC)
  • 清理整个堆的回收(Full GC)

一般的,发生 Major GC 时同时一般也会发生 Minor GC,所以 Major GC 约等于 Full GC。

对象分配过程见JVM之对象的创建过程

分代收集算法调优原则:

  • 合理设置 survivor 区域的大小,避免内存浪费;
  • GC尽量发生在新生代,尽量减少 Full GC 的发生。

相关的JVM参数

image-20220311162205506

6 增量算法

如果内存非常大,如果一次性回收所有垃圾,势必会消耗很多的时间,从而造成系统长时间停摆。

  1. 每次只收集一小片区域的内存空间的垃圾

链接

  1. 垃圾回收

    什么场景下该使用什么垃圾回收策略?垃圾回收发生在哪些区域?对象在什么时候能够被回收?

  2. JVM分代收集理论

    与章节 5 相关,主要介绍分代收集算法实现的理论依据

  3. JVM-内存结构

    主要是基于分代收集理论的JVM内存结构简单分析,包括堆、虚拟机栈、本地方法栈、程序计数器和方法区

概述

  • 什么场景下该使用什么垃圾回收策略?
  • 垃圾回收发生在哪些区域?
  • 对象在什么时候能够被回收?

什么场景下该使用什么垃圾回收策略?

  • 场景一:在对内存要求苛刻的场景:想办法提高对象的回收效率,多回收掉一些对象,腾出更多内存;
  • 场景二:在CPU 使用率高的场景下:降低高并发垃圾回收的频率,让 CPU 更多地去执行你的业务而不是垃圾回收;

垃圾回收发生在哪些区域?

JVM 内存结构如下图所示:

JVM内存结构

  • 虚拟机栈、本地方法栈、程序计数器是线程隔离的,这 3 个区域与线程的生命周期保持一致,同生共死,是不需要考虑垃圾回收的。
  • 堆和方法区市线程共享的,才需要考虑。

对象在什么时候能够被回收?

就目前来说,有两种算法区判断算法什么时候回收。

引用计数法

通过对象的引用计数器来判断对象是否被引用;

如下图所示,对象被引用一次,计数器就加 1 ,但是一旦遇到有循环引用的情况,对象就无法被回收。

image-20220301153530892

可达性分析

Java并没有采用引用计数法,而是使用了可达性分析。

可达性分析:以根对象(GC Roots)作为起点向下搜索,走过的路径被称为引用链(Reference Chain),如果某个对象到根对象没有引用链相连时,就认为这个对象是不可大的,可以被回收。

如下图所示:

可达性分析

GC Roots 包括哪些对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即 native 方法)引用的对象

那什么是引用?

强引用
  • 形如 Object obj = new Object() 的引用
  • 只要强引用在,永远不会回收被引用的对象,哪怕是内存溢出
软引用
  • 形如 SoftReference<String> str = new SoftReference<String>("hello")
  • 是用来描述一些有用但非必需的对象
  • 软引用关联的对象只有在内存不足的时候才会回收
弱引用
  • 形如 WeakReference<String> str = new WeakReference<String>("hello")
  • 弱引用也用来描述非必需对象的
  • 被弱引用关联的对象只能生存到下一次垃圾收集发生为止
  • 无论当前内存是否足够,都会回收掉只被弱引用关联的对象
虚引用
  • 形如

    1
    2
    ReferenceQueue<String> queue = new ReferenceQueue<>();
    PhantomReference<String> pr = new PhantomReference<>("hello", queue);
  • 个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。

  • 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

可达性分析注意点

一个对象即使不可达,也不一定会被回收

即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

image-20220301162604333

  • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

  • 如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize() 方法。这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。
    这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.fengxuechao.jvm;
/**
* 此代码演示了两点:
* * 1.对象可以在被GC时自我拯救。
* * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
*
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;

public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}

public void isAlive() {
System.out.println("yes, i am still alive :)");
}

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
}

结果展示:

image-20220301163844090

事实上假如将代码第24行也就是第二次调用 System.gc() 前没有将SAVE_HOOK 设为 null,那么 SAVE_HOOK 对象将永远不会被回收。

finalize() 的建议

  • 避免使用finalize()方法,操作不当可能会导致问题;

  • finalize() 优先级低,何时会被调用无法确定,因为什么时间发生 GC 不确定;

  • 建议使用 try…catch…finally来替代 finalize()。

NIDD概述

NIDD: Non-IP Data Delivery

NIDD 的功能可用于处理非结构化的数据(也被称为非IP)的移动发起(MO)和移动(MT)通信 。这种对AF的输送是通过以下两种机制之一完成的:

  • 通过NIDD的API传输

  • 使用UPF基于点对点的N6通道传输

NIDD使用一个非结构化的PDU会话来处理NEF。UE可以在PDU会话建立过程中获得一个非结构化的PDU会话到NEF。NIDD API是否应该为一个PDU会话调用,取决于订阅中DNN/S-NSSAI组合的“NIDD的NEF标识”的存在。如果订阅包含与DNN和S-NSSAI信息对应的“NIDD的NEF标识”,则SMF选择该NEF并为该PDU会话使用NIDD API。

如果没有启用RDS (Reliable Data Service)服务,NEF将使用配置的策略将AF Identifier和UE Identity映射到DNN/S-NSSAI组合。如果启用了RDS, NEF将根据RDS端口号和可能用于将AF Identifier和User identity映射到DNN的策略来确定关联关系。

NEF还支持基于NIDD API向一组终端分发移动终止消息。如果在MT NIDD请求中包含外部组标识符,NEF使用UDM将外部组标识符解析为一个SUPIs列表,并将消息发送给组中的每个终端,并建立一个PDU会话。

协议配置选项(PCO)可用于传输NIDD参数进出终端(如最大包大小)。在UE和SMF之间的5GSM信令中,PCO被发送。NIDD参数通过N29接口发送给NEF,也通过NIDD接口发送给NEF。

通过NEF触发NIDD配置

如果NEF收到SMF的NIDD连接建立请求,如果终端没有NIDD配置,NEF可能会向AF发送NIDD配置触发,NEF根据本地配置确定目的URI。NEF应向确定的目标URL发送一个HTTP POST请求,该请求应包含一个NiddConfiguarationTrigger数据类型:

  • NEF标识符,
  • AF标识符,和
  • gpsi作为终端标识。

AF应该用一个HTTP 200 OK响应来确认HTTP POST请求。然后,AF可以按照章节4.4.12.3中的描述启动NIDD配置过程。

通过AF和NIDD delivery触发NIDD配置

3GPP TS 29.122[4]第4.4.5小节描述了AF触发NIDD配置和NIDD交付的过程,但有以下差异:

  • SCS/AS的描述适用于AF;
  • 对SCEF的描述适用于NEF;
  • MME/SGSN的描述适用于SMF;
  • 对于连接的建立,NEF和SMF之间的交互应使用3GPP TS 29.541[24]中规定的Nnef_SMContext服务;
  • 对于MO NIDD, SMF和NEF之间的交互应使用3GPP TS 29.541[24]中规定的Nnef_SMContext服务;和
  • 对于MT NIDD, SMF和NEF之间的交互应使用3GPP TS 29.542[25]中规定的Nsmf_NIDD服务。

基于NIDD的NEF的相关程序

SMF-NEF 建立连接

当终端使用“非结构化”PDU Session类型建立PDU Session时,终端请求的DNN对应的订阅信息包含“NEF Identity for NIDD”(NEF ID),然后,SMF向DNN / S-NSSAI组合的“NEF ID”对应的NEF发起SMF-NEF连接建立过程。

image-20220301145433879

  1. 步骤1-7和步骤9是非漫游场景下PDU会话建立过程;步骤1-9是漫游场景下的建立过程

  2. 如果DNN和S-NSSAI对应的订阅信息包含“NIDD的NEF标识”(NEF ID),则SMF应向NEF创建一个PDU会话。SMF向NEF调用Nnef_SMContext_Create Request (User Identity, PDU Session ID, SMF ID, NIDD信息,S-NSSAI, DNN, [RDS support indication], [Small Data Rate Control parameters], [Small Data Rate Control Status], [Serving PLMN Rate Control parameters])消息。如果在PDU会话建立请求消息中的PCO中包含支持可靠数据服务(RDS)的UE能力,则包括RDS支持指示。如果需要,SMF向NEF提供PDU会话的小数据速率控制参数。如果从AMF接收到SMF,则SMF向NEF提供小数据率控制状态(Small Data Rate Control Status)。如果服务PLMN打算对这个PDU会话实施服务PLMN速率控制(参见TS 23.501[2]的5.31.14.2条款),则SMF应向NEF提供服务PLMN速率控制参数,用于限制下行控制平面数据包的速率。

    如果之前没有AF使用NEF对步骤2中收到的用户身份执行NIDD配置过程,那么NEF在步骤3之前启动NIDD配置过程(参见4.25.3)。

  3. NEF创建一个NEF PDU session Context,并将其与User Identity和PDU session ID相关联。NEF调用Nnef_SMContext_Create Response (Cause, [RDS support indication], [Extended Buffering support indication], [NIDD parameters])向SMF确认UE的PDU会话建立。如果NEF支持并允许使用RDS,则它包括对SMF的RDS支持指示,而SMF将其包含在PCO中。如果NEF支持扩展缓冲,NEF在响应中包括扩展缓冲支持指示,并使用AMF订阅与移动相关的事件,以便在终端可达时接收指示。如果可用,NIDD参数(例如,最大数据包大小)将发送给SMF。

  4. 非漫游场景下,4.3.2.2.1条款第11-13步骤,home- routing漫游场景下,4.3.2.2.2条款第13-16步骤,UE请求PDU会话建立步骤。

非IP数据传输配置

通过NIDD API下发数据时,配置必要信息的流程如图4.25.3-1所示。

NIDD配置过程可以是NEF 发起或AF 触发: NEF发起从第1步开始,AF触发从第2步开始。

image-20220303145547497

  1. [可选]如果 NEF 对给定的 AF 要求 NIDD 配置,则 NEF 向 AF 发送 Nnef_NIDDConfiguration_TriggerNotify (GPSI, AF 标识, NEF ID)消息,请求对 GPSI 识别的 UE 进行 Nnef_NIDDConfiguration_Create 请求。

  2. AF 向 NEF 发送一个 Nnef_NIDDConfiguration_Create 请求消息( GPSI 或外部组标识、AF标识、NIDD持续时间、T8目的地址、请求动作、TLTRI、可靠数据服务配置、MTC提供方信息)。

    可靠数据业务配置是一个可选参数,用于配置可靠数据业务(如TS 23.501[2]第5.31.6条定义),包括发起方应用和接收方应用的端口号。如果请求动作被设置为“Update”或“Cancel”,则请求中包含TLTRI,否则请求中不包含TLTRI, NEF会将TLTRI分配给NIDD配置。如果存在可靠数据业务配置,可靠数据业务配置可能包括 MO 和 MT 的序列化格式,这些格式由AF为每个端口号支持。

  • 注1:NIDD 持续时间是否可以设置为永不过期是由 AF 决定的。
  • 注2:预计 AF 将被配置去使用的 NEF 与 SMF 在 UE 建立基于 NEF 的 NIDD 的 PDU 会话时所选择的 NEF 相同。
  • 注3:当一个 PDU Session 有多个 AF 关联时,步骤 2 中提供的参数可以根据操作符策略或配置在 NEF 中配置。在这种情况下,步骤 2 中提供的任何与预置值冲突的参数都将被忽略。
  • 注4:如果 AF 没有选择序列化格式,则假设 UE 应用程序知道 MT 通信将使用哪种序列化格式,或者 AF 将使用与 MO 通信相同的格式。
  1. 如果请求动作被设置为“取消”;它表明请求的目的是取消由TLTRI标识的事务,然后流继续进行第7步。如果被请求的动作设置为"Update",事务的目的是更新与配置相关的参数(即可靠数据服务)。否则,请求为新的NIDD配置,NEF存储收到的GPSI或外部组标识符、AF标识符、T8目的地址和NIDD持续时间。如果AF没有被授权执行此请求(例如,基于策略,如果SLA不允许),或者Nnef_NIDDConfiguration_Create请求格式不正确,NEF执行第7步,并提供一个Cause值适当地指示错误。根据配置的不同,NEF可能会更改NIDD持续时间。
  2. NEF 向 UDM 发送一个Nudm_NIDDAuthorisation_Get Request ( GPSI 或 External Group Identifier, S-NSSAI, DNN, AF Identifier, MTC Provider Information)消息,授权NIDD配置请求接收到外部组标识符或GPSI。
  • 注5:NEF使用在步骤2中获得的AF标识符、外部组标识符或GPSI来确定将使用什么DNN来实现终端和AF之间的非结构化数据传输。这种确定是基于本地策略的。

  • 注6:步骤2中的“MTC Provider Information”为可选参数。NEF应该验证所提供的MTC提供者信息,并可以根据配置将其覆盖到NEF选择的MTC提供者信息。NEF如何确定MTC提供者信息,如果在步骤2中没有出现,则留给实现(例如,基于请求的AF)。

  1. UDM检查Nudm_NIDDAuthorisation_Get请求消息。

    如果授权成功,并且步骤4中包含外部组标识符,则UDM将外部组标识符映射到内部组标识符和GPSIs列表,并将GPSIs映射到SUPIs。

  • 注7:如果在第4步中包含了外部组标识符,那么当多个GPSI与同一个SUPI相关联时,UDM如何选择一个GPSI,则有待实现,例如,根据MTC提供商信息(如果收到了)或默认GPSI(如果没有收到)。
  1. UDM 向 NEF 发送一个Nudm_NIDDAuthorisation_Get 响应消息(单值或(SUPI和GPSI)、Result的列表),以确认接受了Nudm_NIDDAuthorisation_Get请求。如果UDM确定列表的大小超过了消息的容量,UDM将对列表进行分段,并将其以多个消息的形式发送(分段的详细信息见TS 29.503[52])。在此响应中,UDM返回SUPI(s)和GPSI(s)(如果可用的话)(当Nnef_NIDDConfiguration_Create Request包含GPSI时)。这允许NEF将该过程第2步中收到的AF请求与为每个UE或每个组成员UE建立的SMF-NEF连接关联起来。
  2. NEF向AF发送Nnef_NIDDConfiguration_Create 响应消息 (TLTRI、最大包大小、可靠数据服务指示和原因),以确认接受Nnef_NIDDConfiguration_Create请求。如果NIDD配置被接受,NEF分配一个TLTRI NIDD配置并将其发送给AF, NEF 在TLTRI、GPSI或外部组标识符,SUPI, PDU会话ID之间创建了一个关联关系, 是从SMF-NEF连接过程的步骤2中的 SMF 收到的(见图4.25.2-1: SMF-NEF Connection Procedure)。在MT NIDD过程中,NEF将使用TLTRI和GPSI或External Group Identifier来确定用于发送非结构化数据的PDU会话的SUPI(s)和PDU会话ID。在MO NIDD过程中,NEF将使用SUPI(s)和PDU会话ID(s)来获得TLTRI, GPSI。“可靠数据服务指示”表示NIDD配置中是否启用了“可靠数据服务”。“Maximum Packet Size”是NIDD报文的最大大小。

非IP数据传输支持NEF锚定上行(MO)数据传输

image-20220309110731767

  1. UE根据控制平面CIoT 5GS优化中UPF锚定上行数据传输的步骤1-4发送带有非结构化数据的NAS消息(见4.24.1条款)。如果启用了可靠数据服务,则包含可靠数据服务头。
  2. [可选] 家用路由漫游时,V-SMF向H-SMF发送Nsmf_PDUSession_TransferMOData请求,包括上行(MO)小数据。
  3. (H-)SMF向NEF发送Nnef_SMContext_Delivery Request (User Identity, PDU会话ID,非结构化数据)消息。
  4. 当NEF接收非结构化数据,并找到一个NEF PDU会话上下文和相关T8目的地址,那么它将非结构化数据发送到AF所确定的T8目的地址在Nnef_NIDD_DeliveryNotify请求(GPSI,非结构化数据,可靠的数据服务配置)。如果终端的PDN连接没有关联T8目的地址,则丢弃数据,不发送Nnef_NIDD_DeliveryNotify请求,从步骤6继续流。当启用可靠数据服务时,可靠数据服务配置用于为AF提供额外的信息,如表明确认是否被请求,以及发起者应用程序和接收者应用程序的端口号。
  5. AF以Nnef_NIDD_DeliveryNotify Response (Cause)响应NEF。
  6. NEF向SMF发送Nnef_SMContext_Delivery Response。如果NEF无法交付数据,例如由于AF配置缺失,NEF会向SMF发送适当的错误码。
  7. [可选]家用路由漫游时,H-SMF响应V-SMF的Nsmf_PDUSession_TransferMOData (Result Indication)响应。

NEF 锚定下行(MT)数据传输

image-20220309135200190

1a. 如果AF已经激活了某个终端的NIDD服务,并且有下行非结构化数据要发送给终端,AF会向NEF发送一个Nnef_NIDD_Delivery Request (GPSI, TLTRI,非结构化数据,可靠数据服务配置)消息。可靠数据服务配置是一个可选参数,用于配置可靠数据服务,它可以用来指示是否请求可靠数据服务的确认以及发起方应用程序和接收方应用程序的端口号。

1b. AMF向NEF表示该UE已可达。在此基础上,NEF重新开始向UE交付缓冲的非结构化数据。

  1. NEF根据与NIDD配置和用户身份相关联的DNN确定5GS QoS流上下文。如果在第1步中发现了与GPSI相对应的NEF 5GS QoS Flow Context,则NEF检查AF是否被授权发送数据,以及是否没有超过其配额或速率。如果这些检查失败,则跳过步骤3-15,并在步骤17中返回适当的错误代码。

  2. NEF使用Nsmf_NIDD_Delivery Request将非结构化数据转发给(H-)SMF。如果NEF在建立SMF-NEF连接期间在Nnef_SMContext_Create Response中表示支持扩展缓冲,那么NEF将保留一份数据的副本。NEF使用Nsmf_NIDD_Delivery Request将非结构化数据转发给(H-)SMF。如果NEF在建立SMF-NEF连接期间在Nnef_SMContext_Create Response中表示支持扩展缓冲,那么NEF将保留一份数据的副本。

  3. 漫游时,H-SMF向V-SMF发送Nsmf_PDUSession_TransferMTData,包括下行(MT)小数据。

  4. (V-)SMF根据本地策略和在建立SMF-NEF连接时NEF是否在Nnef_SMContext_Create Response中表示支持扩展缓冲来决定是否应用扩展缓冲。(V-)SMF压缩报头,通过Namf_Communication_N1N2MessageTransfer服务操作将数据和PDU会话ID转发给AMF。如果扩展缓冲应用,那么(V-SMF)在Namf_Communication_N1N2Message Transfer中包含“扩展缓冲支持”的指示。

  5. 如果AMF确定SMF无法访问终端(例如,如果终端处于MICO模式或终端被配置为扩展空闲模式DRX),那么AMF拒绝来自SMF的请求。如果SMF没有订阅UE可达事件,AMF可以在拒绝消息中包括指示SMF不需要向AMF触发Namf_Communication_N1N2MessageTransfer请求。

    如果SMF包含扩展缓冲支持指示,AMF在拒绝消息中表示SMF确定扩展缓冲时间的估计最大等待时间。当终端处于MICO模式时,AMF会根据下次预期的定时注册定时器更新过期时间或实际执行情况来确定“最大等待时间预估”。如果终端配置为扩展空闲模式DRX, AMF将根据下一个PagingTime Window的启动时间来确定“估计最大等待时间”。AMF会存储SMF已被告知UE不可达的指示。

  6. 漫游时,V-SMF向H-SMF发送Nsmf_PDUSession_TransferMTData (Result Indication)响应。如果V-SMF从AMF收到一个“估计的最大等待时间”,并且应用了扩展缓冲,V-SMF也会把这个“估计的最大等待时间”传递给H-SMF。

  7. 如果(H-)SMF收到故障提示,(H-)SMF也会向NEF发送故障提示。如果(H-)SMF收到了“估计的最大等待时间”并且应用了扩展缓冲,(H-)SMF在故障指示中包含扩展缓冲时间。扩展缓冲时间由(H-)SMF决定,应该大于或等于估计的最大等待时间。NEF为扩展缓冲时间存储DL数据。如果收到后续的下行数据包,NEF不会再发送任何Nsmf_NIDD_Delivery Request消息。这个过程在这个步骤停止。

  8. 如果AMF在步骤5中确定终端可达,则应用控制平面CIoT 5GS优化程序(第4.24.2条)中UPF锚定的移动终端数据传输步骤3至6。
    如果可靠数据服务头指示确认被请求,则UE应对接收到的DL数据响应确认。

  9. 如果AMF在第9步中对终端进行了分页触发NAS过程,AMF应启动第4.2.4.2条中定义的UE配置更新过程,以分配一个新的5G-GUTI。

  10. 如果终端对寻呼没有响应,AMF会向(V-)SMF发送寻呼失败通知。否则,该过程在步骤13继续。

  11. 在漫游情况下,如果V-SMF收到AMF的故障通知,则V-SMF向H-SMF发送Nsmf_PDUSession_TransferMTData (Result Indication)响应。

  12. 如果(H-)SMF收到失败通知,则SMF指示NEF请求的Nsmf_NIDD_Delivery失败。如果应用扩展缓冲,则NEF将清除数据副本。这个过程在第17步继续。

  13. 在控制平面CIoT 5GS优化程序(第4.24.2条)中应用UPF锚定的移动终端数据传输的步骤9至11。

  14. AMF通知(V-)SMF数据已被转发。

  15. 漫游时,V-SMF向H-SMF发送Nsmf_PDUSession_TransferMTData (Result Indication)响应,表示数据已被转发

  16. (H-)SMF表示数据已转发给NEF。如果应用扩展缓冲,则NEF将清除数据副本。

  17. NEF向AF发送一个Nnef_NIDD_Delivery Response (cause)。
    可靠数据服务确认指示用于指示是否从终端收到了对MT NIDD的确认。在步骤1中如果可靠数据服务请求,然后Nnef_NIDD_Delivery响应发送到AF从问题或收到确认后,如果没有收到确认,那么Nnef_NIDD_Delivery响应发送到AF导致价值表明还没有收到任何确认。

NIDD 授权更新

image-20220309152114864

  1. UDM可以使用Nudm_NIDDAuthorisation_UpdateNotify Request (SUPI, GPSI, S-NSSAI, DNN, Result)消息向NEF发送NIDD授权更新信息来更新用户的NIDD授权。

  2. NEF向UDM发送Nudm_NIDDAuthorisation_UpdateNotify Response (cause)消息以确认授权更新。

  3. 如果授权被删除,NEF应启动第4.25.8条规定的SMF-NEF连接释放程序。

  4. NEF通过向AF发送Nnef_NIDDConfiguration_UpdateNotify Request (GPSI, TLTRI, Result)消息来通知AF用户的授权状态已经改变。

  5. AF用Nnef_NIDDConfiguration_UpdateNotify Response消息响应NEF。

SMF 启动 SMF-NEF 连接释放

image-20220309154936239

当PDU会话释放被启动,并且如果NEF被选择为控制平面CIoT的锚定,5GS优化启用了非结构化PDU会话类型,如第4.3.4.2条所述,然后SMF对该DNN / S-NSSAI组合对应的“NEF ID”对应的NEF启动一个SMF-NEF Connection Release过程。

  1. SMF执行4.3.4.2章节中的“PDU会话释放步骤”中的步骤1。

  2. 如果NEF被选为锚定控制平面CIoT 5 g的优化使PDU会话的非结构化PDU会话类型如4.3.2.2条款所述,SMF发起SMF-NEF连接释放这个PDU会话发送Nnef_SMContext_Delete请求(用户标识、PDU会话ID、S-NSSAI款,释放原因(Release Cause)消息给NEF。

  3. NEF删除与User Identity和PDU Session ID相关联的NEF PDU Session Context。NEF发送Nnef_SMContext_Delete Response (Cause, [Small Data Control Rate Status], [APN Rate Control Status])给终端确认释放SMF-NEF会话。如果PDU会话使用小数据速率控制,NEF包含“小数据速率控制状态”。

  4. 4.3.4.2章节中的“PDU Session释放步骤”中的步骤3-15。

NEF 启动 SMF-NEF 连接释放

在以下情况下,NEF会启动SMF-NEF连接释放过程:
—当UDM的NIDD授权更新请求表示该用户不再被授权使用NIDD时,或者

  • failure of AF或failure of AF connection,或

  • 基于AF的请求。

图4.25.8-1描述了NEF Initiated SMF-NEF Connection Release procedure(基于AF的请求)。

image-20220309164017922

  1. AF可以通过向NEF调用Nnef_NIDDConfiguration_Delete Request (TLTRI)来指示用户的NIDD SMF-NEF连接不再需要。

  2. NEF删除与TLTRI相关的NEF PDU session Context,并通过调用Nnef_NIDDConfiguration_Delete Response对AF的响应来确认NIDD配置的删除。

  3. NEF通过向SMF调用Nnef_SMContext_DeleteNotify Request来通知SM上下文信息的删除。

  4. SMF通过对NEF调用Nnef_SMContext_DeleteNotify Response来确认该通知。

  5. 如果不再需要PDU会话,SMF将执行PDU会话释放步骤(参见4.3.4.2)中的步骤2-11。

基于NIDD Authorization Update的NEF Initiated SMF-NEF Connection Release流程如图4.25.8-2所示。

image-20220309164127290

  1. 在UDM的NIDD授权更新中,NEF可能决定它需要释放相应的SMF-NEF连接。
  2. NEF会删除对应的NEF PDU session Context,并通过对SMF调用Nnef_SMContext_DeleteNotify Request来通知SM上下文信息的删除。
  3. SMF通过对NEF调用Nnef_SMContext_DeleteNotify Response来确认该通知。
  4. 如果不再需要PDU会话,SMF将执行PDU会话释放步骤(参见4.3.4.2)中的步骤2-11。

NEF 锚定组NIDD通过NEF锚定单播MT数据

图4.25.9-1描述了AF发送组NIDD到外部组标识符的过程。在第4.25.3条规定的NIDD配置过程中,NEF已经在UDM的帮助下解决了外部组标识符到单个supi的映射,这是一个先决条件。NEF在第4.25.5条中规定的独立MT NIDD程序被NEF重用,以单播MT数据到每个终端。

image-20220309164647308

  1. 如果AF已经使用NIDD配置程序条款4.25.3激活NIDD服务的问题,非结构化数据发送到组被外部组标识符,AF发送一个Nnef_NIDD_Delivery请求(外部组标识符、TLTRI非结构化数据,可靠数据服务配置)消息发送给NEF。“可靠数据服务配置”为可选参数,用于配置“可靠数据服务”。当非结构化数据被发送到外部组标识符时,AF不应在可靠数据服务配置中请求确认。
  2. 根据现有的终端组NIDD配置(参见4.25.3),NEF向AF发送单个Nnef_NIDD_Delivery Response,以确认步骤1中的组NIDD delivery请求已被接受。
  3. NEF使用NEF锚定的移动终端数据传输程序(在第4.25.5条的步骤2-16中指定)向组中的每个终端发送相同的MT NIDD。
  4. 对组内所有终端执行步骤3后,NEF以Nnef_NIDD_GroupDeliveryNotify消息发送聚合响应。如果由于UE省电导致某些目标UE无法到达,那么NEF不会缓冲MT NIDD,但在这一步中,它可以在响应AF时包括这些UE的预期可达性的指示。如果向特定终端交付失败,NEF中可能会包含cause值。

[TOC]

1 基本架构概述

https://img-blog.csdnimg.cn/311f0d0b4e094ce38436b88815fcc82e.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiA5bm05pil5Y-I5p2l,size_20,color_FFFFFF,t_70,g_se,x_16SQL语句在MySQL中的执行流程

  • MySQL 分为两层,Server 层和存储引擎层:

  • Server层:

    • 主要包括连接器、查询缓存、分析器、优化器和执行器等,
    • 还有一些跨存储引擎的功能也在这里,如:存储过程、视图、函数和通用日志模块binglog等。
  • 存储引擎;

    • 主要负责数据的读写,它提供了读写IO,
    • 采用了可替换的插件式架构,支持 InnoDB(有自有的redolog日志模块,默认引擎)、MyISAM、Memory等
  • 查询流程:

    1. 客户端请求
    2. 连接器(验证用户身份,给予权限)
    3. 查询缓存(存在缓存则直接返回,不存在则执行后续操作)
    4. 分析器(对SQL语句进行词法分析和语法分析)
    5. 优化器(主要对执行的SQL优化选择最优的执行方案)
    6. 执行期(执行时先验证有无权限,有则使用指定引擎提供的接口)
    7. 存储引擎(获取返回的数据,如果开启查询缓存则会缓存查询结果)

2 组件介绍

2.1 连接器

连接器主要和身份认证和权限相关的功能相关,主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作。

值得注意的是,身份认证和权限校验这些操作,连接器会去数据库权限表中查询该用户的所有权限,之后只要连接不中断,后面的所有SQL语句都会以连接中的权限去执行,及时我们修改了权限。

2.2 查询缓存

查询缓存主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集。

连接建立后,执行查询语句时,会先去查询缓存,MySQL会先校验这个SQL是否执行过,以 Key-Value 的形式缓存在内存中。如果 Key 被命中,则直接返回给客户端,如果没有命中,则执行后续操作后把结果缓存起来。

MySQL 8.0 版本后删除了这个缓存功能,可能是因为查询缓存失效在实际业务场景中可能会非常频繁,例如我们对一个表执行更新操作,那么这个表的所有查询缓存都会失效。

2.3 分析器

分析器主要用来分析 SQL 语句是来干嘛的,它有这些步骤:

  1. 词法分析:提取一条SQL语句中的关键子,如:SELECT、查询的表、字段名、查询条件;
  2. 语法分析:判断SQL语句是否正确,符合语法。

2.4 优化器

优化器的作用就是它认为的最优的执行方案去执行,比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等。

2.5 执行器

当选择了执行方案后,MySQL 就准备开始执行了,首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。

[TOC]

1 编译器优化

1.1 字节码是如何运行的?

  • 解释执行:由解释器一行一行翻译执行

    • 优势在于没有编译的等待时间
    • 性能相对差一些
  • 编译执行:把字节码编译成机器码,直接执行机器码

    • 运行效率会高很多,一般认为比解释执行快一个数量级
    • 带来了额外的开销

那么如何查看自己的java是解释执行还是编译执行呢?

1
2
3
4
$ java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

mixed mode 代表混合执行,部分解释执行、部分编译执行。

  • -Xint:设置JVM的执行模式为解释执行模式

    1
    2
    3
    4
    $ java -Xint -version
    java version "1.8.0_251"
    Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
    Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, interpreted mode)
  • -Xcomp:JVM优先以编译模式运行,不能编译的,以解释模式运行

    1
    2
    3
    4
    $ java -Xcomp -version
    java version "1.8.0_251"
    Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
    Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, compiled mode)
  • -Xmixed:以混合模式运行

一般情况下,我们的代码一开始一般由解释器解释执行。但是当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会认为这些代码是热点代码(如何定位?)。为了提高热点代码的执行效率,会用即使编译器(也就是JIT)把这些热点代码编译城与本地平台相关的机器码,并进行各层次的优化(操作系统的不同、CPU架构的不同)

1.2 Hotspot 的即时编译器 C1

  • 是一个简单快速的编译器
  • 主要关注局部性的优化
  • 适用于执行时间较短或启动性能有要求的程序。例如。GUI应用对界面启动速度就有一定要求。、
  • 也被称为 Client Compiler

1.3 Htospot 的即时编译器 C2

  • 是为长期运行的服务器端应用程序做性能调优的编译器
  • 适用于执行时间较长或对峰值性能有要求的程序
  • 也被称为是 Server Compiler

1.4 分层编译

从JDK7开始,正式引入了分层编译的概念,可以细分为 5 种编译级别:

    1. 解释执行
    1. 简单 C1 编译:会用 C1 编译器进行一些简单的优化,不开启 Profiling(JVM性能监控)
    1. 受限的 C1 编译:仅执行带方法调用次数以及循环回边执行次数Profiling的 C1 编译
    1. 完全C1编译:会执行带有所有Profiling的C1代码
    1. C2 编译:使用C2编译器进行优化,该级别会启用一些编译耗时较长的优化,一些情况下会根据性能监控信息进行一些非常激进的性能优化

级别越高,应用启动越慢,优化的开销越高,峰值性能也越高。

1.5 分层编译- JVM参数配置示例

  • 只想开启 C2:-XX:-TieredCompilation(禁用中间编译层(123层))
  • 只想开启 C1:-XX:+TieredCompilation -XX:TieredStopAtLevel=1

1.6 如何找到热点代码?思路?

  • 基于采样的热点探测

    周期性检查各个线程的栈顶,如果发现某一些方法总是出现在各个栈顶,那就说明是热点代码。

  • 基于计数器的热点探测

    大致思路是为每一个方法甚至是代码块建立计数器,然后统计执行的次数,如果超过一定的阈值,那就说明它是热点代码。Hotspot虚拟机采用的就是基于计数器的热点探测。

1.7 Hotspot 内置的两类计数器

  • 方法调用计数器(Invocation Counter)

    用于统计方法被调用的次数,在不开启分层编译的情况下,在 C1 编译器下的默认阈值是 1500 次,在 C2 模式下是 10000次。也可以哦那个 -XX:CompileThreshold=X 指定阈值

  • 回边计数器(Back Edge Counter)

    • 用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。在不开启分层编译的情况下,C1 编译器心爱的默认阈值 13995,C2 默认为 10700,可使用 -XX:OnStackReplacePercentage=X指定阈值
    • 建立回边计数器的主要目的是为了触发 OSR (OnStackReplacement)编译,参考文档(https://www.zhihu.com/question/45910849/answer/100636125
  • 当开启分层编译时,JVM会根据当前编译的方法数以及编译线程数来动态调整阈值,-XX:CompileThreshold、-XX:OnStackReplacePercentage 都会失效。

1.8 方法调用计数器流程

image-20210809233815237

如果不做任何设置,方法调用次数统计的并不是方法被调用的绝对次数,而是一个相对的执行频,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数荏苒不足以让它提交给及时编译器编译,那这个方法的调用计数器就会减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。进行热度衰减的动作是在虚拟机进行垃圾手机是顺便进行的,可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。

1.9 回边计数器流程

image-20210809234920524

1.10 方法内联

1.10.1 什么是方法内联?示例?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example;

public class InlineTest1 {
private static int add1(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}

public static int add2(int x1, int x2) {
return x1 + x2;
}

// 内联后
private static int addInline(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
}

所谓的方法内联就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用,从而减少压栈和出栈的作用

1.10.2 发生方法内联的条件

  1. 方法体足够小

    热点方法:如果方法体小于325字节会尝试内联,可用 -XX:FreqInlineSize 修改方法体大小。

    非热点方法:如果方法体小于35字节,也会尝试内联,可用-XX:MaxInlineSize修改大小。

  2. 被调用方法运行时的实现可唯一确定

    static方法、private方法及final方法,JIT可以唯一确定具体的实现代码。

    public的实例方法,指向的实现可能是自身、父类、子类的代码,当且仅当JIT能够唯一确定方法的具体实现时,才有可能完成内联。

1.10.3 使用方法内联的注意点:

  1. 尽量让方法体小一点;
  2. 尽量使用 final、private、static关键字修饰方法,避免因为多态,需要对方法做额外检查;
  3. 在某些场景下,可通过JVM 参数修改阈值,从而让更多方法内联。

1.10.4 方法内联可能带来的问题

内联是用空间换时间的一种做法,也就是及时编译器在方法调用期间把方法调用连接在一起,但是经过内联的代码会变多,而增加的代码量取决于方法的调用次数以及方法本身的大小。

在一些极端情况下,内联可能会导致

  • CodeCache(热点代码的缓存区,及时编译代码和本地方法代码)的溢出,导致JVM退化成解释执行模式;

1.10.5 内联相关JVM参数

image-20220215160030958

image-20220215160113222

1.10.6 方法内联测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class InlineTest {

private final static Logger log = LoggerFactory.getLogger(InlineTest.class);

public static void main(String[] args) {
long cost = compute();
log.info("执行花费 {} ms", cost);
}

private static long compute() {
long start = System.currentTimeMillis();
int result;
Random random = new Random();
for (int i = 0; i< 10000000; i++) {
int a = random.nextInt();
int b = random.nextInt();
int c = random.nextInt();
int d = random.nextInt();
result = add1(a, b, c, d);
}
long end = System.currentTimeMillis();
return end - start;
}

public static int add1(int n1, int n2, int n3, int n4) {
return add2(n1, n2) + add2(n3, n4);
}

private static int add2(int n1, int n2) {
return n1 + n2;
}
}

内联启动JVM参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

image-20220215163048392

不内联启动JVM参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:+FreqInlineSize=1

image-20220215162940763

但是,一般来说不建议使用这些JVM参数,默认的就好,现代JVM是相当智能的。

1.11 逃逸分析、标量替换、栈上分配

1.11.1 逃逸分析

逃逸分析:分析变量能否逃出它的作用域。

可以细分为 4 种场景:

  1. 全局变量赋值逃逸
  2. 方法返回值逃逸
  3. 实例引用逃逸
  4. 线程逃逸
    • 赋值给类变量或可以在其他线程中访问的实例变量

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @author fengxuechao
* @date 2022/2/15
*/
public class EscapeTest1 {

public static SomeClass someClass;

/**
* 全局变量赋值逃逸
*/
public void globalVariablePointerEscape() {
someClass = new SomeClass();
}

/**
* 方法返回值逃逸
* someMethod() {
* SomeClass someClass = methodPointerEscape();
* }
*/
public SomeClass methodPointerEscape() {
return new SomeClass();
}

/**
* 实例引用传递逃逸
*/
public void instancePassPointerEscape() {
this.methodPointerEscape()
.printClassName(this);
}

}

class SomeClass {
public void printClassName(EscapeTest1 escapeTest) {
System.out.println(escapeTest.getClass().getName());
}
}

JVM 会针对以上4种场景进行分析,然后会为对象做一个逃逸状态标识,一个对象主要有 3 种逃逸状态标识:

  • 全局级别逃逸:一个对象可能从方法或者当前线程中逃逸,主要有以下几种场景:
    1. 对象被作为方法的返回值;
    2. 对象作为静态字段(static field)或者成员变量(field);
    3. 如果重写了某个类的 finialze()方法,那么这个类的对象都会被标记为全局逃逸状态并且一定会放在堆内存中。
  • 参数级别逃逸
    1. 对象被作为参数传递给一个方法,但是在这个方法之外无法访问/对其他线程不可见;
  • 无逃逸状态:一个对象不会逃逸

1.11.2 标量替换

标量:不能被进一步分解的量

  • 基础数据类型
  • 对象引用

聚合量:可以进一步分解的量

  • 字符串

那么什么是标量替换呢?逃逸分析确定该对象不会被外部访问,并且对象可以进一步被分解,JVM不会创建该对象,而是创建它的成员变量来代替。

代码展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
int id;
int age;
}

public static void main(String[] args) {
Person person = new Person();
person.id = 1;
person.age = 18;

// 上面这段代码,标量替换后,原本的对象就不用分配内存空间了,就会优化成如下所示:
int id = 1;
int age = 18;
}

JVM 参数:-XX:+EliminateAllocaions 开启标量替换(JDK8 默认开启)

1.11.3 栈上分配

在 Java 中,绝大多数对象都是放在堆里面的,当对象无用时,由垃圾回收器回收。

栈上分配,顾名思义,就是通过逃逸分析,能够确认对象不会被外部访问,就在栈上分配对象,在栈上分配对象,就可以在栈帧出栈的时候销毁对象了,通过栈上分配就可以降低垃圾回收的压力。

1.11.4 相关JVM参数

image-20220215212537078

2 参考资源

深入Feign 体系架构、底层机制、动态代理、重试

构建请求

  • ribbon、hystrix:引入feign的同时,ribbon和hystrix这两个组件也会被一同引入。

    • ribbon:利用负载均衡策略选定目标机器
    • hystrix:根据熔断器的开启状态,决定是否发起此次调用
  • 动态代理

    Feign是通过一个代理接口进行远程调用,这一步就是为了构造接口的动态代理对象,用来代理远程服务的真实调用,这样你就可以像调用本地方法一样发起HTTP请求,不需要像Ribbon或者Eureka那样在方法调用的地方提供服务名。在Feign中动态代理是通过Feign.build返回的构造器来装配相关参数,然后调用ReflectFeignnewInstance方法创建的。这里就应用到了Builder设计模式。

  • Contract

    协议,顾名思义,就像HTTP协议,RPC协议一样,Feign也有自己的一套协议的规范,只不过他解析的不是HTTP请求,而是上一步提到的动态代理类。通过解析动态代理接口+Builder模式,Contract协议会构造复杂的元数据对象MethodMetadata,这里面包含了动态代理接口定义的所有特征。接下来,根据这些元数据生成一系列MethodHandler对象用来处理Request和Response请求。

    • Contract具有高度可扩展性,可以经由对Contract的扩展,将Feign集成到其他开源组件之中。

发起调用

  • 拦截器

    拦截器是Spring处理网络请求的经典方案,Feign这里也沿用了这个做法,通过一系列的拦截器对Request和Response对象进行装饰,比如通过RequestInterceptor给Request对象构造请求头。整装待发之后,就是正式发起调用的时候了。

  • 发起请求

    又到了Ribbon和Hystrix的出场镜头了。这哼哈二将绝不放过开头和结尾两处重要镜头,正所谓从头到尾都参与了进来。

    • 重试

      Feign这里借助Ribbon的配置重试器实现了重试操作,可以指定对当前服务节点发起重试,也可以让Feign换一个服务节点重试。

    • 降级

      Feign接口在声明时可以指定Hystrix的降级策略实现类,如果达到了Hystrix的超时判定,或得到了异常结果,将执行指定的降级逻辑。Hystrix降级熔断的内容,将在下一个大章节和大家见面。

参考

慕课网

image-20220120124433404

分代收集理论

当前大多数的虚拟机都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

1)弱分代假说:绝大多数对象都是朝生夕灭的。

2)强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

这两个假说共同奠定了多款常用垃圾收集器的设计原则:

收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

弱分代假说

如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;

强分代假说

如果剩下的都是难以消亡的对象,那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

从这里开始就可以对Java 堆划分不同的区域了,垃圾收集器才可以每次只回收其中某一个或或者某些部分的区域——因而才会有“Minor GC”“Major GC”“Full GC”这样的回收类型的划分。

故此,垃圾收集至少有新生代和老年代。

但是,根据实际情况,对象与对象之间并不是孤立的,对象之间存在跨代引用。新生代对象完全有可能被年老代对象引用。

由此,就有了第三条经验法则:“跨代引用假说”。

跨代引用假说

跨代引用相对于同代引用来说仅占极少数。

存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。

举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

  • 如何对存在跨代引用的对象进行回收?

    我们就不应再为了少量的跨代引用去扫?整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

垃圾收集分类

  • 部分收集:指目标不是完整收集整个Java堆的垃圾收集
    • 新生代收集:指目标只是新生代的垃圾收集
    • 老年代收集:指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
    • 混合收集:指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
  • 整堆收集:收集整个Java堆和方法区的垃圾收集。

参考文档

《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》 - 3.3.1