G1详解

时间:2024-04-16 12:52:05

一 G1收集器

  • g1收集器是一个面向服务端的垃圾收集器适用于多核处理器、大内存容量的服务端系统。
  • 它满足短时间gc停顿的同时达到一个较高的吞吐量。
  • JDK7以上版本适用

先介绍两个概念:吞吐量和响应能力,响应能力和吞吐量是评价一个系统的两个重要指标

吞吐量

  • 吞吐量关注的是,在一个指定的时间内,最大化一个应用的工作量。
  • 如下方式来衡量一个系统吞吐量的好坏:
  • 在一定时间内同一个事务(或者任务、请求)完成的次数(tps)
  • 数据库一定时间以完成多少次查询
  • 对于关注吞吐量的系统,一定次数卡顿(即stw)是可以接受的,因为这个系统关注长时间的大量任务的执行能力,单次快速的响应并不值得考虑

响应能力

响应能力指一个程序或者系统对请求是否能够及时响应,比如:

  • 一个桌面UI能多快地响应一个事件
  • 一个网站能够多快返回一个页面请求
  • 数据库能够多快返回查询的数据

对于这类对响应能力敏感的场景,长时间的停顿是无法接受的,关注每一次反应的能力。

1.1 G1收集器的设计目标

与应用线程同时工作,几乎不需要 stop the work(与CMS类似);

  • G1的设计规划是要替换掉CMS,G1在某些方面弥补了CMS的不足,比如CMS使用的是mark-sweep算法,自然会产生内存碎片(CMS只能在Full GC时,用 stop the world整理内存碎片),然而G1基于copying算法,高效的整理剩余内存,而不需要管理内存碎片

  • GC停顿更加可控,甚至可以设置一个时间阈值,比如说可以回收某些部分的老年代,而不像CMS老年代必须全部回收(其它收集器时间可能难以把握)

1.2 G1重要概念

1.2.1 分区( Region)

  • G1采取了不同的策略来解决并行、串行和CMS收集器的碎片、暂停时间不可控等问题—G1将整个堆分成相同大小的分区或称为区域( Region)

    • G1的堆结构如下:
      • 1582976958190
    • 每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。年轻代,survivor区,老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。分区在物理上不需要连续,则带来了额外的好处,可以在老年代中只对某些区域(Region)进行回收。对于新生代依然是在新生代满了的时候,对整个新生代进行回收一一整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小【其实新生代并不适合这样的垃圾收集,因为是对新生代的所有区域进行回收!不用像老年代那样选择回收效益高的Region】
    • 在G1中,还有一种特殊的区域,叫 Humongous区域。如果一个对象占用的空间达到或是超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个 Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC

1.2.2 收集集合(CSet)

收集集合(Collection Set):一组可被回收的分区的集合,在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自eden空间、 survivor空间、或者老年代,并且可以同时包含这几代分区的内容。

1.2.3 已记忆集合(RSet)

已记忆集合(Remember Set):RSet使区域Region的并行和独立成为可能,RSet记录了其他Region中的对象引用本 Region中对象的关系,属于 points-into结构(谁引用了我的对象)。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。

point-into Rset解析

G1 GC是在 points-out的 card table之上再加了一层结构来构成 points-into Rset:每个Region会使用point-into Rset记录下到底哪些别的Region有指向自己的指针,而这些指针分别在哪些card的范围内【也就是说 card table 记录自己指向了谁,Point-into Ret 记录了谁指向了自己】。这个RSet其实是一个 hash table,key是别的Region的起始地址,value是一个集合,里面的元素是 card table的 Index.举例来说,如果region A的Rset里有一项的key是 region B,value里有 index为1234的card,它的意思就是 region B的一个card里有引用指向region A。所以对 region A来说该RSet记录的是 points-into的关系,而 card table仍然记录了 points-out的关系。

car table 解析

card table是什么又是怎么来的呢?如果一个对象引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1中又引入了另外一个概念,卡表( Card Table)。一个 Card Table将一个分区在逻辑上划分为固定大小的连续区域每个区域称之为卡。卡通常较小,介于128到512字节之间。 Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址

下图表示了RSet、Card和Region的关系):

1583938173624

上图中有三个Region,每个Region被分成了多个Card,在不同Region中的Card会相互引用,Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points- into。

而维系RSet中的引用关系靠post-write barrier和Concurrent refinement threads来维护,操作伪代码如下:

void oop_field_store(oop* field, oop new_value) {
  pre_write_barrier(field);             // pre-write barrier: for maintaining SATB invariant
  *field = new_value;                   // the actual store
  post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
}

RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。

1.2.4 SATB

Snapshot-at-the-beginning(SATB)SATB是G1 GC在全局并发标记阶段使用的增量式的标记算法。并发标记是并发多线程的,但并发线程在同一时刻只扫描一个分区

1.3 GC模式

G1提供了两种GC模式, Young GC和 Mixed Gc两种都是完全 Stop The World的

1.3.1 Young GC

Young GC:将选择所有年轻代里的 Region进行GC(所以CSet里存的就是所有年轻代里面的Region)。将存活的对象复制到幸survivor区是多线程执行的并且需要stw,如果满足晋升到老年代阈值,则某些对象将被提升到老年代,并通过动态计算出下次GC时堆中年轻代的 Region个数,即年轻代内存大小来控制 Young GC的时间开销。

Young GC的触发时机

Young GC在Eden充满时触发,在回收之后所有之前属于Eden的区块全部变成空白即不属于任何一个分区(Eden、 Survivor、Old)

1.3.2 Mixed GC

Mixed GC:将选择所有年轻代里的 Region,外加根据 global concurrent marking统计得出收集收益高的若干老年代 Region进行GC(所以CSet里存的是所有年轻代里的 Region加上在全局并发标记阶段标记出来的收益高的 Region)。在用户指定的开销目标范围内尽可能选择收益高的老年代 Region,以此来控制Mixed GC的时间开销。

  • 需要注意的是:Mixed GC不是 Full GC,它只能回收部分老年代的 Region而不是全部的Region,如果 Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行 Mixed GC,就会退化然后使用 serial old GC( Full GC)来收集整个 GC heap。所以本质上,G1是不提供 Full GC的。所以总的来说,G1的运行过程是这样的:会在 Young GC和Mixed GC之间不断地切换运行,同时定期地做全局并发标记,在实在赶不上对象创建速度的情况下使用Full Gc( Serial GC)。
  • 它的GC步骤分为两步全局并发标记(global concurrent marking)和拷贝存活对象(evacuation)

Mixed gc 中的Global concurrent marking

Mixed gc 中的 global concurrent marking的执行过程类似于CMS,但是不同的是,在G1 GC中,它主要是为 Mixed GC提供标记服务的【老年代之所以知道为什么哪里的回收效益高就是根据此服务得出的结果确定的】,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为四个步骤

  • 初始标记( initial mark,STW):它标记了从GC Root开始直接可达的对象。第一阶段 initial mark是共用了 Young GC的暂停,这是因为他们可以复用 root scan操作,所以可以说 global concurrent marking是伴随 Young GC而发生的。
  • 并发标记( Concurrent Marking):这个阶段从GC Root开始对heap中的对象进行标记,标记线程与应用程序线程并发执行,并且收集各个Region的存活对象信息。
  • 重新标记( Remark,STW) :标记那些在并发标记阶段发生变化的对象,将被回收。
  • 清理( Cleanup ):清除空 Region(没有存活对象的),加入到 free list。第四阶段 Cleanup只是回收了没有存活对象的 Region,所以它并不需要STW

停顿预测模型

  • G1收集器突出表现出来的一点是通过一个停顿预测模型根据用户配置的停顿时间来选择CSet的大小,从而达到用户期待的应用程序暂停时间
  • 通过-XX:MaxgcpauseMillis 参数来设置。这一点有点类似于 Parallel Scavenge收集器。关于停顿时间的设置并不是越短越好,设置的时间越短意味着每次收集的CSet越小,导致垃圾逐步积累变多,最终不得不退化成 Serial GC;停顿时间设置的过长那么会导致每次都会产生长时间的停顿影响了程序对外的响应时间

Mixed GC的触发时机

触发时机由一些参数控制,另外这些参数也控制着哪些老年代 Region会被选入CSet(收集集合)

  • 参数:G1heapwastepercent: Global concurrent marking结束之后,我们可以知道 old gen regions中有多少空间要被回收在每次YGC之后和再次发生Mixed GC之前【YGC是在Eden空间满了就进行GC】会检査垃圾占比是否达到此参数,只有达到了,下次才会发生 Mixed GC
  • 参数:G1MixedGCliveThresholdPercent: old generation region中的存活对象的占比,只有占比低于此参数,该old generation region才会被选入CSet
  • 参数:G1MixedGCCountTarget:一次global concurrent marking之后,最多执行 MixedGC的次数
  • 参数:G1OldCSetRegionThresholdPercent:一次Mxed GC中能被选入Cset的最多old generation region数量
  • 1583073184480

1.3.3 G1的收集概览

G1算法将堆划分为若干个区域( Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代和老年代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在

YGC收集时如何找到根对象呢?老年代所有的对象都是根对象吗?那如果直接对整个老年代进行扫描的话会耗费大量的时间。于是G1引入了我们上面提到的RSet的概念,作用是跟踪指向某个heap区内的对象引用。但是在新生代之间记录引用吗?这是不必要的,原因在于每次GC时所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。YGC收集过程如下:,不懂的原因,对Rset 和point out point into 相关还是不理解

1583157404318

三色标记算法

这是Mixed gc 中的Global concurrent marking中的内容,提到并发标记,我们不得不了解并发标记的三色标记算法,mark的过程就是遍历heap标记 live object的过程。它是描述追踪式回收器的一种有效的方法,利用它可以推演回收器的正确性。我们将对象分成三种类型:

  • 黑色:根对象或者该对象与它的子对象都被扫描过(即对象被标记了,且它的所有feld也被标记完了)
  • 灰色:对象本身被扫描,但还没扫描完该对象中的子对象(即它的 field还没有被标记或标记完)
  • 白色:未被扫描对象,如果扫描完成所有对象之后最终为白色的为不可达对象,即垃圾对象(即对象没有被标记到)

标记过程演示

  • 1583131636331
  • 1583131729874
  • 1583131855524

可能出现的问题

  • 1583132245202

  • 这时候应用程序执行了以下操作A.c=C和B.c=null这样,对象的状态图变成如下情形

  • 1583132314470

  • 这时候垃圾收集器再去标记扫描的时候就会变成下图这样,这样就会造成漏标:

  • 1583132429116

解决方法-SATB

SATB snapshot at the beginning

  • 在开始的时候生成一个快照图,标记存活的对象
  • 在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的,如上图中,将B所指向的引用C变成非白的)
  • 可能存在浮动垃圾,将在下次垃圾收集时被收集

SATB详解

  • SATB详解SATB是维持并发GC的一种手段。G1并发的基础就是SATB。SATB可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对象就认为是活的,从而形成个对象图。在GC收集的时候,新生代的对象也认为是活的对象,除此之外其他不可达的对象都认为是垃圾对象

  • 如何找到在GC过程中分配的对象呢? 这是收集过程中可能遇到的第二个问题,每个Region记录着两个top-at-mark- start(TAMS)指针,分别为 prevtamst和 next AMS。在TAMS以上的对象就是新分配的,因而被视为隐式 marked。通过这种方式我们就找到了在GC过程中新分配的对象,并把这些对象认为是活的对象

  • 解决了对象在GC过程中分配的问题,那么在GC过程中引用发生变化的问题怎么解决呢?Write Barrier就是对引用字段进行赋值做额外处理。通过 Write Barrier就可以了解到哪些引用对象发生了什么样的变化。

  • SATB仅仅对于在 marking开始阶段进行snapshot"(marked all reachable at markstart)),但是 concurrent的时候并发修改可能造成对象漏标记

    • 对 black新引用了一个 white对象,然后又从gray对象中删除了对该 white对象的引用这样会造成了该whie对象漏标记
    • 对 black新引用了一个 white对象,然后从gray对象删了一个引用该 white对象的 white对象,这样也会造成了该 white对象漏标记
    • 对 black新引用了一个刚new出来的 white对象,没有其他gray对象引用该 White对象这样也会造成了该 white对象漏标记
    • 对于三色算法在 concurrent的时候可能产生的漏标记问题,SATB在 marking阶段中对于从gray对象移除的目标引用对象标记为gray,对于 black引用的新产生的对象标记为 black;由于是在开始的时候进行snapshot,因而可能存在 Floating Garbage
  • 漏标与误标

    • 误标没什么关系,顶多造成浮动垃圾,在下次gc还是可以回收的,但是漏标的后果是致命的把本应该存活的对象给回收了,从而影响的程序的正确性
    • 漏标的情况只会发生在白色对象中,且满足以下任意一个条件
      • 并发标记时,应用线程给一个黑色对象的引用类型字段赋值了该白色对象
      • 并发标记时,应用线程删除所有灰色对象到该白色对象的引用(但是此时或许有黑色对象引用该白色对象)
      • 如何解决以上问题?
        • 对于第一种情况,利用 post-write barrier,记录所有新增的引用关系,然后根据这些引用关系为根重新扫描一遍,Write Barrier就是对引用字段进行赋值做额外处理。通过 Write Barrier就可以了解到哪些引用对象发生了什么样的变化
        • 对于第二种情况,利用pre-write barrier,将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根重新扫描遍,这样就能扫描标记到那个“白色对象”

1.4 G1最佳实践

1.4.1 不断调优暂停时间指标

通过 XX. MaxGcPauseMillis=x 可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择Cset来满足响应时间的设置。一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成 Full gc。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。

1.4.2 不要设置新生代和老年代的大小

G1收集器在运行的时候会调整新生代和老年代的大小。通过改变代的大小来调整对象晋升的速度以及晋升年龄,从而达到我们为收集器设置的暂停时间目标。设置了新生代大小相当于放弃了G1为我们做的自动调优。我们需要做的只是设置整个堆内存的大小,剩下的交给G1自己去分配各个代的大小即可

1.4.3 关注 Evacuation Failure

Evacuation Failure【Evacuation是拷贝存活对象的意思,是GC步骤之一,GC步骤分为两步全局并发标记(global concurrent marking)和拷贝存活对象(evacuation)】 类似于CMS里面的晋升失败堆空间的垃圾太多导致无法完成 Region之间的拷贝,于是不得不退化成 Full GC来做一次全局范围内的垃圾收集

对比其它收集器

  • G1 VS CMS对比使用mark- sweep的CMS,G1使用的copying算法不会造成内存碎片
  • 对比 Parallel Scavenge(基于 copying)Parallel Old收集器(基于mark- compact-sweep), Parallel会对整个区域做整理导致gc停顿会比较长,而G1只是特定地整理压缩某些Region
    • G1并非一个实时的收集器,与 parallel Scavenge一样,对gc停顿时间的设置并不绝对生效,只是G1有较高的几率保证不超过设定的gc停顿时间。与之前的gc收集器对比,G1会根据用户设定的gc停顿时间智能评估哪几个 region需要被回收可以满足用户的设定

1.5 实验

实验代码

实验参数

实验结果

2020-03-03T11:06:20.055+0800: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0024128 secs]
   [Parallel Time: 1.3 ms, GC Workers: 8]
   # GC Workers: 8 ,当前的GC线程数为8
      [GC Worker Start (ms): Min: 178.9, Avg: 178.9, Max: 178.9, Diff: 0.1]
      # GC线程开始
      [Ext Root Scanning (ms): Min: 0.3, Avg: 0.4, Max: 0.5, Diff: 0.3, Sum: 3.2]
   	  # 根节点扫描
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      # 更新RSet
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.6, Avg: 0.7, Max: 0.8, Diff: 0.2, Sum: 5.8]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      # 处理引用队列
         [Termination Attempts: Min: 1, Avg: 6.0, Max: 9, Diff: 8, Sum: 48]
    # 此以上的步骤都是和之前说到的YGC的处理流程是一一对应的
      [GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.9]
      [GC Worker Total (ms): Min: 1.2, Avg: 1.2, Max: 1.3, Diff: 0.1, Sum: 10.0]
      [GC Worker End (ms): Min: 180.2, Avg: 180.2, Max: 180.2, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   # 即clear CartTable 
   [Other: 1.0 ms]
      [Choose CSet: 0.0 ms]
      # 选择要回收的Region
      [Ref Proc: 0.1 ms]
      [Ref Enq: 0.0 ms]
      # 引用的处理
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 2048.0K(6144.0K)->0.0B(2048.0K) Survivors: 0.0B->1024.0K Heap: 3990.1K(10.0M)->2847.6K(10.0M)]
   # 表示执行完YGC之后,整个eden空间的结果,可以看到执行完之后eden空间变成了0;Survivor空间变成1m,这是从新生代来的
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-03-03T11:06:20.058+0800: [GC concurrent-root-region-scan-start]
2020-03-03T11:06:20.059+0800: [GC pause (G1 Humongous Allocation) (young)2020-03-03T11:06:20.059+0800: [GC concurrent-root-region-scan-end, 0.0011467 secs]
2020-03-03T11:06:20.059+0800: [GC concurrent-mark-start]
, 0.0019177 secs]
# 以上是并发的一些处理
   [Root Region Scan Waiting: 0.5 ms]
   [Parallel Time: 0.9 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 182.5, Avg: 182.6, Max: 182.8, Diff: 0.3]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 1.2]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.4, Avg: 0.4, Max: 0.5, Diff: 0.1, Sum: 3.5]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
         [Termination Attempts: Min: 1, Avg: 4.8, Max: 8, Diff: 7, Sum: 38]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 0.4, Avg: 0.7, Max: 0.9, Diff: 0.4, Sum: 5.2]
      [GC Worker End (ms): Min: 183.3, Avg: 183.3, Max: 183.4, Diff: 0.1]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.4 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.2 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 1024.0K(2048.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 3912.6K(10.0M)->3895.4K(10.0M)]
   # 第二次YGC的结果
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-03-03T11:06:20.061+0800: [GC concurrent-mark-end, 0.0015466 secs]
2020-03-03T11:06:20.061+0800: [Full GC (Allocation Failure)  3895K->3731K(10M), 0.0042062 secs]
   [Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->0.0B Heap: 3895.4K(10.0M)->3731.8K(10.0M)], [Metaspace: 3113K->3113K(1056768K)]
 [Times: user=0.03 sys=0.00, real=0.00 secs] 
2020-03-03T11:06:20.066+0800: [GC remark, 0.0000313 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-03-03T11:06:20.066+0800: [GC concurrent-mark-abort]
完成了
Heap
 garbage-first heap   total 10240K, used 4755K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
  region size 1024K, 1 young (1024K), 0 survivors (0K)
  # 堆的空间分配情况
 Metaspace       used 3238K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

1.6 参考文章