如何判断对象可以被回收

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的
对象)

引用计数法

给对象添加一个引用计数器,每当有一个地方引用,计数器就加1。当引用失效,计数器就减1。任何时候计数器为0
的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中没有选择这个算法来管理内存,最主要的原因是它很难解决对
象之前相互循环引用的问题。所谓对象之间的相互引用问题,通过下面代码所示:除了对象a和b相互引用着对方之
外,这两个对象之间再无任何引用。但是它们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数器
法无法通知GC回收器回收它们。

可达性分析算法

这个算法的基本思想就是通过一系列的称为”GC Roots“的对象作为起点,从这些节点开始向下搜索,节点所走过的路
径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象时不可用的。
GC Roots根节点:类加载器、Thread、虚拟机栈的局部变量表、static成员、常量引用、本地方法栈的变量等等

如何判断一个常量是废弃常量
运行时常量池主要回收的是废弃的常量。那么,我们怎么判断一个常量时废弃常量呢?
假如在常量池中存在字符串"abc",如果当前没有任何String对象引用该字符串常量的话,就说明常量”abc“就是废弃
常量,如果这时发生内存回收的话而且有必要的话,”abc“会被系统清理出常量池。

如何判断一个类是无用的类

需要满足以下三个条件:
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里仅仅是”可以“,而并不是和对象一样不适用了就必然会被回
收。
怎样让一个对象自救/或一个类不被回收:重写finalize()方法,在finalize()方法里持有给对象的引用
注:finalize 是 object 类的一个方法,在垃圾收集器执行的时候会调用这个对象回收的方法,工垃圾收集时其他资源的回收,比如关闭文件。

垃圾回收算法

标记-清除算法

它是最基础的收集算法,这个算法分为两个阶段,“标记”和”清除“。首先标记出所有需要回收的对象,在标记完成后
统一回收所有被标记的对象。它有两个不足的地方:
1. 效率问题,标记和清除两个过程的效率都不高;
2. 空间问题,标记清除后会产生大量不连续的碎片;

复制算法

为了解决效率问题,复制算法出现了。它可以把内存分为大小相同的两块,每次只使用其中的一块。当这一块的内存
使用完后,就将还存活的对象复制到另一块区,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内
存区间的一半进行回收

标记-整理算法

根据老年代的特点提出的一种标记算法,标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行
回收,而是让所有存活的对象向一段移动,然后直接清理掉边界以外的内存

分代收集算法

现在的商用虚拟机的垃圾收集器基本都采用"分代收集"算法,这种算法就是根据对象存活周期的不同将内存分为几
块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
在新生代中,每次收集都有大量对象死去,所以可以选择复制算法,只要付出少量对象的复制成本就可以完成每次垃
圾收集。而老年代的对象存活几率时比较高的,而且没有额外的空间对它进行分配担保,就必须选择“标记-清除”或
者“标记-整理”算法进行垃圾收集

垃圾收集器

java虚拟机规范对垃圾收集器应该如何实现没有任何规定,因为没有所谓最好的垃圾收集器出现,更不会有万金油垃
圾收集器,只能是根据具体的应用场景选择合适的垃圾收集器。

Serial收集器

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收
集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进
行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法。

虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩
短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)
Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集器
对于运行在Client模式下的虚拟机来说是个不错的选择。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集
算法、回收策略等等)和Serial收集器完全一样。
新生代采用复制算法,老年代采用标记-整理算法。

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并
发收集器,后面会介绍到)配合工作。

Parallel Scavenge收集器(JDK1.8)

Parallel Scavenge 收集器类似于ParNew 收集器。
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停
顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel
Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,
手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用复制算法,老年代采用标记-整理算法。

Serial Old收集器

Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本
中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以
优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

CMS收集器

并行和并发概念补充:

  • 并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序
    在继续运行,而垃圾收集器运行在另一个CPU上。
    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用
    户体验的应用上使用。
    CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾
    收集线程与用户线程(基本上)同时工作。
    从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几
    种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
  • 初始标记(CMS initial mark): 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快
  • 并发标记(CMS concurrent mark): 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶
    段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC
    线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记(CMS remark): 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生
    变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶
    段时间短
  • 并发清除(CMS concurrent sweep): 开启用户线程,同时GC线程开始对为标记的区域做清扫。

    CMS主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
  • 对CPU资源敏感;
  • 无法处理浮动垃圾;
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

G1收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足
GC停顿时间要求的同时,还具备高吞吐量性能特征.

被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备一下特点:

  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短StopThe-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方
    式让java程序继续执行
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。 空间整
    合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基
    于“复制”算法实现的
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追
    求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内
    G1收集器的运作大致分为以下几个步骤:
  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收
    G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名
    字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间
    内可以尽可能高的收集效率(把内存化整为零)。

怎么选择垃圾收集器?

  1. 优先调整堆的大小让服务器自己来选择
  2. 如果内存小于100m,使用串行收集器
  3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
  4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选
  5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器
    官方推荐G1,性能高。

JDK性能调优监控工具

Jinfo

查看正在运行的Java程序的扩展参数

查看JVM的参数

查看java系统属性

等同于System.getProperties()

Jstat

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令格式:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]

Jmap

可以用来查看内存信息

堆的对象统计


如图:

  • Num:序号
  • Instances:实例数量
  • Bytes:占用空间大小
  • Class Name:类名

堆信息

堆内存dump


jmap -dump:format=b,file=temp.hprof
也可以在设置内存溢出的时候自动导出dump文件(项目内存很大的时候,可能会导不出来)
1.-XX:+HeapDumpOnOutOfMemoryError
2.-XX:HeapDumpPath=输出路径

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -
XX:HeapDumpPath=d:\oomdump.dump


可以使用jvisualvm命令工具导入文件分析

在这里可以看到OutOfMemoryError,以及导致该错误的线程

Jstack

jstack用于生成java虚拟机当前时刻的线程快照。

上面截取线程快照的一段:其中Thread-1:线程1 prio=5:在JVM的优先级别为5 os:镜像 prio=0:操作系统线程级别 tid:JVM线程id nid:对应操作系统id , 等待第10行0x00000000d5fbdld8锁资源,而自己拥有了第11行0x00000000d5fbdld8锁资源
快照底部会有些造成死锁的原因

调优

JVM调优主要就是调整下面两个指标
停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。-XX:MaxGCPauseMillis
吞吐量:垃圾收集的时间和总时间的占比:1/(1+n),吞吐量为1-1/(1+n)。-XX:GCTimeRatio=n

GC调优步骤

1.打印GC日志

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log

Tomcat可以直接加载JAVA_OPTS变量里

上面是JVM参数,第4开始才是GC日志

前面表示时间戳,
GC表示轻GC,
Allocation Failure:表示分配不均引起的
PSYoungGen: 使用Parallel Scavenge垃圾收集器对轻生代进行回收
33280K->3024K(38400K): 从33280K回收完变成3024K,(38400K):代表总量
33280K->3040K(125952K):表示整个堆的情况:可以看到3024K—>3040K,清GC后,总量变多一点,是由于此次是清YGC,其中在survivor区 部分转入老年代造成的
0.0045285 secs:此次执行所用的时间 后面Times:是总的时间统计
可以看到清YGC时间是非常短的,所以YGC不是我们优化的对象,优化主要在Full GC

举个Full GC例子

Metada GC Threshold:由于元空间引起的
PSYoungGen:使用Parallel Scavenge垃圾收集器对轻生代进行回收(Full GC是对每个区域都回收)
ParOldGen:使用Parallel Old垃圾收集器对老年代进行回收
3569K->5745K(48128K):因为这边产生Full GC不是由老年代引起的,所以轻生代存活的都转到老年代引起数量增加
6097K->5745K(187392K):是整个堆的情况
Metaspace:20553K->20553K(1067008K):此次Full GC是由于元空间引起的,而清之前跟之后都是20553K不变?但总量1067008K还有这么多,那是因为扩容引起的,元空间有个策略:21M水平线,当元空间达到21M时就会产生一次垃圾回收,当回收完后,可用空间圆小于21M,
水平线就会下降,如果回收后,还是21M左右,水平线就水上升,
可以再看个水平线上涨的例子

相比于之前的20553K,这一次涨到33934K,当涨到1079296K时,那就真正的满的,没法再放更多数据了
(如果是由于元空间扩容引起的Full GC,可以直接扩大元空间,减少由于元空间扩容引起的Full GC次数)

2.分析日志得到关键性指标
3.分析GC原因,调优JVM参数
1.Parallel Scavenge收集器(默认)
分析parallel-gc.log
第一次调优,设置Metaspace大小:增大元空间大小-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M
第二次调优,增大年轻代动态扩容增量(默认是20%),可以减少YGC:-XX:YoungGenerationSizeIncrement=30
比较下几次调优效果:

2.配置CMS收集器
-XX:+UseConcMarkSweepGC
分析gc-cms.log
3.配置G1收集器
-XX:+UseG1GC
分析gc-g1.log
young GC:[GC pause (G1 Evacuation Pause)(young)
initial-mark:[GC pause (Metadata GC Threshold)(young)(initial-mark) (参数:InitiatingHeapOccupancyPercent)
mixed GC:[GC pause (G1 Evacuation Pause)(Mixed) (参数:G1HeapWastePercent)
full GC:[Full GC (Allocation Failure)(无可用region)
(G1内部,前面提到的混合GC是非常重要的释放内存机制,它避免了G1出现Region没有可用的情况,否则就会触
发 FullGC事件。CMS、Parallel、Serial GC都需要通过Full GC去压缩老年代并在这个过程中扫描整个老年代。G1的
Full GC算法和Serial GC收集器完全一致。当一个Full GC发生时,整个Java堆执行一个完整的压缩,这样确保了最大
的空余内存可用。G1的Full GC是一个单线程,它可能引起一个长时间的停顿时间,G1的设计目标是减少Full GC,满
足应用性能目标。)
查看发生MixedGC的阈值:jinfo -flag InitiatingHeapOccupancyPercent 进程ID
调优:
第一次调优,设置Metaspace大小:增大元空间大小-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M
第二次调优,添加吞吐量和停顿时间参数:-XX:GCTimeRatio=99 -XX:MaxGCPauseMillis=10

GC常用参数

堆栈设置

-Xss:每个线程的栈大小
-Xms:初始堆大小,默认物理内存的1/64
-Xmx:最大堆大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewSize:设置新生代初始大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
-XX:MetaspaceSize:设置元空间大小
-XX:MaxMetaspaceSize:设置元空间最大允许大小,默认不受限制,JVM Metaspace会进行动态扩展。

垃圾回收统计信息

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

收集器设置

-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParalledlOldGC:设置并行老年代收集器
-XX:+UseConcMarkSweepGC:设置CMS并发收集器
-XX:+UseG1GC:设置G1收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数

并行收集器设置

-XX:ParallelGCThreads:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis:设置并行收集最大暂停时间
-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

CMS收集器设置

-XX:+UseConcMarkSweepGC:设置CMS并发收集器
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads:设置并发收集器新生代收集方式为并行收集时,使用的CPU数。并行收集线程数。
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理

G1收集器设置

-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
-XX:GCTimeRatio:吞吐量大小,0-100的整数(默认9),值为n则系统将花费不超过1/(1+n)的时间用于垃圾收集
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor填充容量(默认50%)
-XX:MaxTenuringThreshold:最大任期阈值(默认15)
-XX:InitiatingHeapOccupancyPercen:老年代占用空间超过整堆比IHOP阈值(默认45%),超过则执行混合收集
-XX:G1HeapWastePercent:堆废物百分比(默认5%)
-XX:G1MixedGCCountTarget:参数混合周期的最大总次数(默认8)

Java虚拟机(三)--------GC算法和收集器相关推荐

  1. 深入理解java虚拟机(三)GC垃圾回收-对象存活算法

    文章目录 前言 一.引用计数算法 二.可达性分析算法 三.了解引用 结尾 前言 在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还&qu ...

  2. Java虚拟机详解05----垃圾收集器及GC参数

    本文主要内容: 堆的回顾 串行收集器 并行收集器 CMS收集器 零.堆的回顾: 新生代中的98%对象都是"朝生夕死"的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一 ...

  3. 深入理解java虚拟机-读书笔记2-垃圾收集器和内存分配策略

    垃圾回收重点区域:堆和方法区部分区域. 引用计数算法: 1,引用计数算法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器都为0的对象 ...

  4. 深入理解java虚拟机(五)GC垃圾回收-经典垃圾收集器

    文章目录 前言 一.Serial收集器(标记-复制算法) 二.ParNew收集器(标记-复制算法) 三.Parallel Scavenge收集器(标记-复制算法) 四.Serial Old收集器(标记 ...

  5. java的垃圾回收机制包括:主流回收算法和收集器(jvm的一个主要优化方向)

    2019独角兽企业重金招聘Python工程师标准>>> java的垃圾回收机制是java语言的一大特色,解放了开发人员对内存的复杂控制,但如果你想要一个高级java开发人员,还是需要 ...

  6. 《深入理解Java虚拟机》-----第3章 垃圾收集器与内存分配策略

    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来. 3.1 概述 说起垃圾收集(Garbage Collection,G ...

  7. 【Android 内存优化】垃圾回收算法 ( 分代收集算法 | Serial 收集器 | ParNew 收集器 | Parallel Scavenge 收集器 | CMS 并发标记清除收集器 )

    文章目录 一. 分代收集算法 二. 垃圾回收器 / 收集器 ( GC ) 三. 串行收集器 ( Serial ) 四. ParNew 收集器 五. Parallel Scavenge 收集器 六. C ...

  8. [JVM-3]Java垃圾回收(GC)机制和垃圾收集器选择

    哪些内存需要回收? 1.引用计数法 这个算法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1:当引用失效时,计数器值-1.任何时刻计数值为0的对象就是不可能再被使用的.这 ...

  9. Java内存组成GC算法

    Java内存组成&GC算法 @(JAVA)[java] Java内存组成GC算法 一内存组成 一Java程序的内存组成 1Java堆 2方法区含常量池永久代 3栈 1Java虚拟机栈 2本地方 ...

最新文章

  1. csdn上传资源提示“该资源已存在,请重新上传”
  2. C语言图形界面的编程
  3. JavaScript中setAttribute用法
  4. Teams数据统计 - 聊天消息
  5. php 安全基础 第七章 验证与授权 永久登录
  6. 怎么样才能更高效的学习区块链
  7. 机器视觉齿轮质量快速检测
  8. java 静态扫描_静态代码扫描工具 – (八)- 扫描Java项目
  9. android中进行https连接的方式的详解
  10. IDEA 部署 Java Web 应用为 war 包
  11. JS Jquery 中 的遍历
  12. linux根据部署jenkins
  13. 用matlab编写指派问题,[原创] Matlab 指派问题模型代码
  14. 脚本重启电信天翼网关
  15. 内网环境下element-template配置element-admin
  16. 2019携程校园招聘编程题(2)取满足条件订单号
  17. 2022CPA审计-第三编-各类交易和账户余额的审计-【完结-有点感觉了?】
  18. 企业微信自建应用获取用户信息
  19. 镜像网络MW受邀亮相巴比特杭州区块链国际周
  20. (My)SQL 使用入门

热门文章

  1. 以鸿蒙为景柱1009无标题,鸿蒙的意思
  2. 租用服务器如何选择带宽,带宽越大越好吗
  3. 发现U盘不显示盘符的解决办法
  4. [fpga基础]基础元器件
  5. 批量转化py2topy3脚本
  6. xp系统电脑蓝屏怎么解决,解决xp电脑屏幕蓝屏
  7. 2013年中南大学复试-惠民工程
  8. 小老弟!听说你在搞Android 10.0 适配,看这篇就妥了!
  9. WinXP_Vista禁止限制软件使用方法
  10. 2021.7纪中快乐游记(下)