jvm:jvm GC日志解析:G1日志解析

1、环境 & 参数配置

1.1、jdk版本(openjdk)

由于oracle声明将针对企业用户对于JDK 8 u191之后的版本需要付费购买商业许可。所以公司统一都切换到了openjdk,目前应用的版本是:openjdk 1.8.0_272-b10。

下载地址:https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/tag/jdk8u272-b10

1.2、运行环境配置

应用部署在docker环境,docker资源配置情况:

  • 物理机:64位
  • cpu:4核
  • 内存:8G

1.3、jvm启动参数配置

因为对外提供了很多核心接口,应用所以对于响应的低延迟是刚需,在选择垃圾收集器上自然就选用G1。以下是具体启动参数配置:

  • -XX:+UseG1GC【选择使用G1垃圾收集器】
  • -Xms4G【初始堆内存4G】
  • -Xmx4G【最大堆内存4G,初始和最大设置成一样,避免堆内存在应用运行过程中自动扩容而影响服务稳定性】
  • -XX:MetaspaceSize=256M【元数据空间的扩容临界值,元数据空间的内存commited指超过256M,将会发生mixed GC和扩容】
  • -XX:MaxMetaspaceSize=1G【元数据空间最大值1G】
  • -XX:MaxDirectMemorySize=1G【这个参数直接干预sun.nio.MaxDirectMemorySize这个属性,会限制nio可用的最大直接内存为1G】
  • -XX:InitialBootClassLoaderMetaspaceSize=256M【给每个classloader分配的元数据空间初始值是256M】
  • -XX:+PrintGCDetails【打印GC日志】
  • -XX:+PrintGCDateStamps【打印GC日志附带时间戳】
  • -XX:+PrintHeapAtGC【GC之前和GC之后的堆的内存使用情况要打印出来】
  • -Xloggc:/data/logs/java/gc.log 【GC日志的存储路径(前缀)】
  • -XX:+UseGCLogFileRotation【GC日志文件开启滚动存储】
  • -XX:NumberOfGCLogFiles=5【GC日志文件最多保留5个】
  • -XX:GCLogFileSize=30M【每个GC日志文件最大30M,超过30M,生成新的文件】
  • -XX:+HeapDumpOnOutOfMemoryError【当发生内存溢出时dump堆转储文件】
  • -XX:HeapDumpPath=/data/logs/java/heap_dump_%p.log【堆转储文件的存放地址】

2、gc日志

2.1、gc日志头

gc日志文件的开头内容如下

OpenJDK 64-Bit Server VM (25.272-b10) for linux-amd64 JRE (1.8.0_272-b10), built on Oct 19 2020 11:11:12 by "openjdk" with gcc 4.4.7 20120313 (Red Hat 4.4.7-23)
Memory: 4k page, physical 8388608k(4903852k free), swap 16777212k(14997524k free)
CommandLine flags: -XX:CompressedClassSpaceSize=536870912 -XX:GCLogFileSize=31457280 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/java/heap_dump_%p.log -XX:InitialBootClassLoaderMetaspaceSize=268435456 -XX:InitialHeapSize=4294967296 -XX:MaxDirectMemorySize=1073741824 -XX:MaxHeapSize=4294967296 -XX:MaxMetaspaceSize=1073741824 -XX:MetaspaceSize=268435456 -XX:NumberOfGCLogFiles=5 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:+UseGCLogFileRotation 

这里分为三部分信息:版本和环境、内存、命令行参数。可以通过hotspot源码片段来印证一下。

源码片段 -> ostream.cpp

// dump vm version, os version, platform info, build id,
// memory usage and command line flags into header
void gcLogFileStream::dump_loggc_header() {
  if (is_open()) {
    print_cr("%s", Abstract_VM_Version::internal_vm_info_string());// 虚拟机信息
    os::print_memory_info(this); // 操作系统内存信息
    print("CommandLine flags: "); 
    CommandLineFlags::printSetFlags(this); // 打印命令行参数
  }
}

接下来分别来看一下这三部分信息。

1)第一行是版本和环境信息

  • 虚拟机的版本信息:OpenJDK 64-Bit Server VM (25.272-b10)
  • 操作系统信息:linux-amd64
  • JRE版本:JRE (1.8.0_272-b10)
  • JDK的构建信息:built on Oct 19 2020 11:11:12 by “openjdk” with gcc 4.4.7 20120313 (Red Hat 4.4.7-23)

2)第二行是内存信息

  • 内存页大小:4K
  • 物理内存:8G(4.7G空闲)、交换分区16G(14.3G空闲)

3)第三行是命令行参数信息

这里面的参数和启动参数之间是存在一些关系和差异的。有一些是和启动参数配置一样的,只是把(G/M/K)数值转化为了字节而已。需要重点关注的是以下两类:

3.1)意义一样名字有变化

-XX:InitialHeapSize 对应启动参数里的 -Xms

-XX:MaxHeapSize 对应启动参数里的 -Xmx

3.2)启动参数里没有设置的但是这里出现的

3.2.1) -XX:+PrintGC

源码片段 -> global.hpp

manageable(bool, PrintGC, false, "Print message at garbage collection") 
                           
manageable(bool, PrintGCDetails, false, "Print more details at garbage collection")   

从这里可以看出PrintGC、PrintGCDetails参数默认值都是false。只有在命令行配置了才会置为true。因为显示配置了PrintGCDetails,所以他肯定为true。

源码片段 -> arguments.cpp -> Arguments::parse_each_vm_init_arg()

// 省略了很多代码
 else if (match_option(option, "-Xloggc:", &tail)) {
    // 省略了很多代码
    FLAG_SET_CMDLINE(bool, PrintGC, true);
    FLAG_SET_CMDLINE(bool, PrintGCTimeStamps, true);
}
// 省略了很多代码

从这里可见如果配置了Xloggc,那么也会默认将PrintGC置为true,这里有个细节,调用FLAG_SET_CMDLINE方法去设置某个参数值,这个参数会被打印在CommandLine flags中。如果通过其他方式直接赋值,逻辑可能能够正常跑通,但是在
CommandLine flags中就可能是不会体现出该参数的。

这里注意,除了FLAG_SET_CMDLINE方法之外,还有FLAG_SET_ERGO和FLAG_SET_MGMT方法设置的参数也可以被打印出来。但是通过调用FLAG_SET_DEFAULT方法进行设置的参数就不会被打印,因为FLAG_SET_DEFAULT是会把参数级别设置为DEFAULT级别。而CommandLineFlags::printSetFlags方法的实现中,只打印非DEFAULT级别的参数。

3.2.2)和元数据空间相关的几个参数
这几个参数关联性比较强,所以放在一起讨论。

  • -XX:+UseCompressedOops【开启对象指针压缩,如果一个对象引用了其他对象,那么就必然会维护一个引用指针,如果开启了这个参数,就意味着指针可被压缩,在64位环境上会被压缩为32位】
  • -XX:+UseCompressedClassPointers【开启类指针压缩,每个堆中的java对象的对象头上都会有一个指向他所属类的一个指针。如果开启了这个参数,就意味着指针可被压缩,在64位环境上会被压缩为32位。这个参数生效的前提是开启了UseCompressedOops,本质上类指针也只是一种特殊的引用指针而已,引用的不是普通对象,是类的运行时对象而已】
  • -XX:CompressedClassSpaceSize=536870912【这个参数生效的前提是,开启了UseCompressedClassPointers,如果没有开启类指针压缩,那么就不会有所谓的压缩类空间。这个CompressedClassSpace里存放的是类的运行时对象。要注意,无论是否开启了类指针压缩,都会有一个地方来存储类的运行时对象,只不过开启了类指针压缩后,会给单独开辟出一块连续的内存空间来存储,不会和普通的元数据空间共享。单独开辟出的空间叫class_capacity,其他元数据对象(方法、常量池等)所共享的空间叫non_class_capacity。这个参数被设置的值是512M,至于为什么是512M,下面通过源码分析】

上面主要介绍了这几个在启动命令中没有设置,但是被虚拟机自动设置的几个参数的具体含义,接下来通过源码,来看下为何这些参数会被设置,以及参数值是如何计算得来。

a)在64位机器上默认会开启UseCompressedOops和UseCompressedClassPointers

源码片段 -> arguments.cpp

void Arguments::set_ergonomics_flags() {
    // 省略了部分代码
    #ifdef _LP64
    set_use_compressed_oops();
    // set_use_compressed_klass_ptrs() must be called after calling
    // set_use_compressed_oops().
    
    // 注释解析:必须先调用set_use_compressed_oops开启了UseCompressedOops才可以调用set_use_compressed_klass_ptrs来开启UseCompressedClassPointers
    set_use_compressed_klass_ptrs();
    #endif // _LP64
    // 省略了部分代码
}

void Arguments::set_use_compressed_oops() {
#ifndef ZERO
#ifdef _LP64
  // MaxHeapSize is not set up properly at this point, but
  // the only value that can override MaxHeapSize if we are
  // to use UseCompressedOops is InitialHeapSize.

  // 注释解析:在这个逻辑点上,MaxHeapSize(-Xmx)可能未设置,所以可以就只能使用InitialHeapSize来赋值给max_heap_size
  size_t max_heap_size = MAX2(MaxHeapSize, InitialHeapSize);
  // 如果max_heap_size小于开启指针压缩的最大阈值(32G)
  if (max_heap_size <= max_heap_for_compressed_oops()) {
#if !defined(COMPILER1) || defined(TIERED)
    if (FLAG_IS_DEFAULT(UseCompressedOops)) {
      // 设置UseCompressedOops值为true,参数级别为ERGO
      FLAG_SET_ERGO(bool, UseCompressedOops, true);
    }
#endif
  }  
  // 省略了部分代码
#endif // _LP64
#endif // ZERO
}


// NOTE: set_use_compressed_klass_ptrs() must be called after calling
// set_use_compressed_oops().
void Arguments::set_use_compressed_klass_ptrs() {
#ifndef ZERO
#ifdef _LP64
  // UseCompressedOops must be on for UseCompressedClassPointers to be on.
  if (!UseCompressedOops) {
    // 如果UseCompressedOops为false,还设置了UseCompressedClassPointers为true,那么会输出一个警告信息,然后把 UseCompressedClassPointers置为false
    if (UseCompressedClassPointers) {
      warning("UseCompressedClassPointers requires UseCompressedOops");
    }
    FLAG_SET_DEFAULT(UseCompressedClassPointers, false);
  } else {
    // 走到这里说明UseCompressedOops为true
    // FLAG_IS_DEFAULT 用来判断某个参数是否是default级别的参数,并不关心参数值是什么
    // 因为UseCompressedClassPointers是default级别参数,所以就可以将UseCompressedClassPointers
    if (FLAG_IS_DEFAULT(UseCompressedClassPointers)) {
      FLAG_SET_ERGO(bool, UseCompressedClassPointers, true);
    }
    // 下面还会校验一下CompressedClassSpaceSize的设置是否过大,如果过大,则还是会关闭UseCompressedClassPointers,这样类的运行时数据结构还是混合存放在元数据区,不会单独开辟空间。
    // Check the CompressedClassSpaceSize to make sure we use compressed klass ptrs.
    if (UseCompressedClassPointers) {
      if (CompressedClassSpaceSize > KlassEncodingMetaspaceMax) {
        warning("CompressedClassSpaceSize is too large for UseCompressedClassPointers");
        FLAG_SET_DEFAULT(UseCompressedClassPointers, false);
      }
    }
  }
#endif // _LP64
#endif // !ZERO
}

b)CompressedClassSpaceSize的值

源码片段 -> globals.hpp

product(uintx, CompressedClassSpaceSize, 1*G,"Maximum size of class area in Metaspace when compressed class pointers are used")  

CompressedClassSpaceSize的默认值是1G(默认级别是DEFAULT),但是这块空间是否会开辟,取决于UseCompressedClassPointers是否为true。通过a)中的源码分析已经得知UseCompressedClassPointers是开启的,所以CompressedClassSpaceSize的空间是会开辟的,但是在GC日志里得到的值是512M,并不是1G,就说明这其中还有其他的计算逻辑。

源码片段 -> metaspace.cpp

void Metaspace::ergo_initialize() {
  
  // 省略了很多代码

  CompressedClassSpaceSize = align_size_down_bounded(CompressedClassSpaceSize, _reserve_alignment);
  // 通过调用set_compressed_class_space_size方法把CompressedClassSpaceSize当前的值拷贝了一份赋值给了
  _compressed_class_space_size变量。
  set_compressed_class_space_size(CompressedClassSpaceSize);

  // VIRTUALSPACEMULTIPLIER是常量为2,InitialBootClassLoaderMetaspaceSize通过启动参数配置的是256M,所以min_metaspace_sz(最小元数据空间)为512M
  uintx min_metaspace_sz =
      VIRTUALSPACEMULTIPLIER * InitialBootClassLoaderMetaspaceSize;
  if (UseCompressedClassPointers) {
    // MaxMetaspaceSize启动参数里配置的是1G
    // (512M + 1G > 1G) 为 true
    if ((min_metaspace_sz + CompressedClassSpaceSize) >  MaxMetaspaceSize) {
      // 如果发现 min_metaspace_sz大于MaxMetaspaceSize,说明MaxMetaspaceSize配置的太小了,虚拟之就直接终止退出了。
      // 所以也就是说MaxMetaspaceSize的配置值要至少大于InitialBootClassLoaderMetaspaceSize的2倍
      if (min_metaspace_sz >= MaxMetaspaceSize) {
        vm_exit_during_initialization("MaxMetaspaceSize is too small.");
      } else {
        // 基于上文提到的启动参数配置,逻辑会走到这部分
        // 会重新把CompressedClassSpaceSize设置为1G - 512M = 512M
        FLAG_SET_ERGO(uintx, CompressedClassSpaceSize,
                      MaxMetaspaceSize - min_metaspace_sz);
      }
    }
  // 如果不开启类指针压缩,并且发现最小元数据空间 大于设置的最大元数据空间,则把初始元数据空间设置为min_metaspace_sz  
  } else if (min_metaspace_sz >= MaxMetaspaceSize) {
    FLAG_SET_ERGO(uintx, InitialBootClassLoaderMetaspaceSize,
                  min_metaspace_sz);
  }

}

2.2、young gc日志

该应用的GC日志中大部分都是young gc日志。即使是发生了混合gc,在整个混合gc周期中也是包含着至少一次young gc的。可以这么粗略的认为:混合gc = 各种并发阶段 + 穿插这n次young gc + n次mixed gc。而且标记为mixed的gc日志的格式和young gc的格式是相同,区别只是在于各种数值。所以重点精细分析young gc日志的内容。而混合gc重点关注的通过gc日志来分析是他的各个组成阶段、以及回收步骤。下面是一个常规的 young gc的日志片段。

{Heap before GC invocations=302 (full 0):
 garbage-first heap   total 4194304K, used 3492794K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 1228 young (2514944K), 45 survivors (92160K)
 Metaspace       used 108788K, capacity 351624K, committed 353612K, reserved 1572864K
  class space    used 12362K, capacity 12998K, committed 13388K, reserved 1048576K

// 华丽分割线

2021-11-06T06:07:37.195+0800: 23577.357: [GC pause (G1 Evacuation Pause) (young), 0.1141917 secs]
   [Parallel Time: 97.6 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 23577359.3, Avg: 23577359.3, Max: 23577359.4, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 3.4, Avg: 4.5, Max: 7.6, Diff: 4.2, Sum: 18.0]
      [Update RS (ms): Min: 12.0, Avg: 14.7, Max: 15.7, Diff: 3.6, Sum: 58.9]
         [Processed Buffers: Min: 32, Avg: 69.8, Max: 98, Diff: 66, Sum: 279]
      [Scan RS (ms): Min: 0.4, Avg: 0.5, Max: 0.5, Diff: 0.0, Sum: 1.9]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
      [Object Copy (ms): Min: 77.2, Avg: 77.6, Max: 77.8, Diff: 0.6, Sum: 310.2]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
      [GC Worker Other (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.4]
      [GC Worker Total (ms): Min: 97.4, Avg: 97.4, Max: 97.5, Diff: 0.1, Sum: 389.8]
      [GC Worker End (ms): Min: 23577456.8, Avg: 23577456.8, Max: 23577456.8, Diff: 0.1]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.8 ms]
   [Other: 15.8 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 9.0 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.2 ms]
      [Humongous Register: 1.5 ms]
      [Humongous Reclaim: 0.1 ms]
      [Free CSet: 3.7 ms]
   [Eden: 2366.0M(2366.0M)->0.0B(2372.0M) Survivors: 92160.0K->86016.0K Heap: 3410.9M(4096.0M)->1055.1M(4096.0M)]

// 华丽分割线

Heap after GC invocations=303 (full 0):
 garbage-first heap   total 4194304K, used 1080394K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 42 young (86016K), 42 survivors (86016K)
 Metaspace       used 108788K, capacity 351624K, committed 353612K, reserved 1572864K
  class space    used 12362K, capacity 12998K, committed 13388K, reserved 1048576K
}
 [Times: user=0.28 sys=0.13, real=0.12 secs] 

一次young gc的日志可以分为三个大部分:gc开始之前堆和元数据信息、gc动作的详细信息、gc结束之后的堆和元数据信息

2.2.1、gc开始之前堆和元数据信息

Heap before GC invocations=302 (full 0):
 garbage-first heap   total 4194304K, used 3492794K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 1228 young (2514944K), 45 survivors (92160K)
 Metaspace       used 108788K, capacity 351624K, committed 353612K, reserved 1572864K
  class space    used 12362K, capacity 12998K, committed 13388K, reserved 1048576K

1)回收次数

Heap before GC invocations=302 (full 0)

invocations=302 说明本次gc开始之前,已经经历过302次垃圾回收。其中full gc是0次。

2)堆的内存使用情况

garbage-first heap   total 4194304K, used 3492794K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  • GC之前的堆,[保留]内存共计4G,使用了3492794K(约3.3G),
  • 中括号中是是三个16进制数值。0x-00000006e0000000 :这个数值共有16个有效值,转化为2进制,就是64个有效值(应用所在物理机的CPU是64位)
    • 第一个数值:0x-00000006e0000000指的是堆保留内存低地址边界
    • 第三个数值:0x-00000007e0000000指的是堆保留内存高地址边界
    • 6e0000000 = 01101111 + 28个0
    • 7e0000000 = 01111111 + 28个0
    • (7e0000000 - 6e0000000) = 1 + 32个0 = 2的32次方 = 4G
      • 因为启动参数-Xmx设置为4G
    • 第二个数值:0x00000006e0204000,普遍的说法是commited内存的高地址边界。
      • 按照这个说法,那么(这个数值 - 堆保留内存低地址边界)的结果应该近似等于used数值(3.5G)。
      • 实际呢:0x6e0204000 - 0x6e0000000 = 0x204000 = 2113536(十进制数值) ≈ 2.01M
        • 这个数值和3.5G相差甚远。所以这个数值不能直接代表commited内存,关于这个数值,在下面 3)部分中有单独的解析。
region size 2048K, 1228 young (2514944K), 45 survivors (92160K)

每个region2M,一共1228的young region (占用了2514944K空间),其中45个是survivors(占用了92160K空间)。在统计的时候,把survivors的region也算到了前面的数值里。

3)中间地址的数值到底代表什么呢?

源码片段 -> g1CollectedHeap.cpp

void G1CollectedHeap::print_on(outputStream* st) const {
  st->print(" %-20s", "garbage-first heap");
  st->print(" total " SIZE_FORMAT "K, used " SIZE_FORMAT "K",
            capacity()/K, used_unlocked()/K);
  // 重点关注这一行          
  st->print(" [" INTPTR_FORMAT ", " INTPTR_FORMAT ", " INTPTR_FORMAT ")",
            _hrm.reserved().start(),
            _hrm.reserved().start() + _hrm.length() + HeapRegion::GrainWords,
            _hrm.reserved().end());
  
  // 省略了部分代码
}

通过上面的代码可以得出

  • _hrm.reserved().start() 对应 0x00000006e0000000
  • _hrm.reserved().end() 对应 0x00000007e0000000
  • _hrm.reserved().start() + _hrm.length() + HeapRegion::GrainWords 对应 0x00000006e0204000

所以重点分析下面这一行代码的逻辑就可以

_hrm.reserved().start() + _hrm.length() + HeapRegion::GrainWords

_hrm.reserved().start()已经明确的了他的值是0x00000006e0000000,那么_hrm.length()和HeapRegion::GrainWords都是什么数值呢?


通过下钻_hrm.length()

源码片段 -> heapRegionManager.hpp

// Return the number of regions that have been committed in the heap.
uint length() const { return _num_committed; }

可以得知,他的返回值是_num_committed,通过注释可以得知这个数值代表的是堆中已条件的region的数量。


通过下钻HeapRegion::GrainWords

源码片段 -> heapRegion.cpp -> HeapRegion::setup_heap_region_size

GrainBytes = (size_t)region_size;
guarantee(GrainWords == 0, "we should only set it once");
GrainWords = GrainBytes >> LogHeapWordSize;

可以得知,GrainWords = GrainBytes >> LogHeapWordSize,其中GrainBytes就是region_size也就是2M,而LogHeapWordSize是常量3,也就是说最后的GrainWords = 2M/8。

从宏观上来看_hrm.length()代表的committed的region数量,而GrainWords代表region的大小,那么正常的一个计算保留内存地址的逻辑应该是

_hrm.reserved().start() + (_hrm.length() * HeapRegion::GrainWords)

似乎更合理。这个暂且不管,就按照源码的公式来计算的话,还差一个_hrm.length()的具体数值不清楚是多少。所以可以逆向的推导一下

_hrm.length() = 0x6e0204000 - _hrm.reserved().start() - HeapRegion::GrainWords

// 等同于
_hrm.length() = 0x6e0204000 - 0x6e0000000 - 2M/8  

如果真是按照上面的等式直接计算的话,会得到的值是十进制2097152。这显然是不对的,因为堆最大内存一共4G,每个region是2M大小,也就是最多也就能有2048个region,而且虚拟机的内部实现也不允许分配超过2048个region,会按照这个2048的上限自动调整region和堆大小。上面之所以会得出一个不正确的committed regions的数量,是因为忽略了_hrm.reserved().start()的类型,这个_hrm.reserved().start()返回的是一个HeapWord*,在c++里这个代表一个指针对象,c++的指针对象参与数值运算时,他本身取的是指针指向的内存地址,而和他运算的其他普通数值要乘以一个基数。这个基础在32位机器上是4,在64为机器上是8. 所以直观上的真实的计算应该是下面这样

_hrm.length() * 8 = 0x6e0204000 - 0x6e0000000 - (2M/8 * 8)  = 16384 
// 等式转化后如下
_hrm.length() = 16384 / 8 = 2048

所以可知道已经committed的region的数量为2048个,虽然使用内存只有3.3G,但是实际上4G的内存已经都被划分为region了。既然所有的region都已经commited了,那么如果非要用内存地址来标识committed内存地址的高边界的话,应该等于0x00000007e0000000。所以中间的这个16进制数的设计和实现逻辑确实给人带来一些困扰。

4)元数据空间使用情况

Metaspace       used 108788K, capacity 351624K, committed 353612K, reserved 1572864K

Metaspace指的是整个元数据空间的内存占用情况(包括class space)

  • used:加载的数据占用108M(约),
  • capacity:包含数据的chunk块共计占用351624K(有些块是没满的)。每个chunk基于level不同,大小在1k-4M之间不等。
  • committed:申请所有的chunk块的总空间353612K(这里包含这未被使用的空的chunk块)。
  • reserved:jvm启动时,基于参数设置,向操作系统申请的预留空间。这里是1.5G(为什么是1.5?下面会讲)。
class space    used 12362K, capacity 12998K, committed 13388K, reserved 1048576K

之所有会有class space这一行日志,是因为前面介绍的UseCompressedClassPointers是开启的。如果UseCompressedClassPointers不是开启的,那么就不会有这一样日志的存在。

class space 见名知意,就存储类(类运行时结构,也叫Klass)的空间,虽然在类指针压缩开启时才会有,但class本身并没有被压缩。因为堆中对象的对象头中的类指针使用了32位的压缩机制,在技术实现上就要求对应的类就必须在一块连续的不超过4G的存储空间上存储(为什么?)。

这是因为采用指针压缩,必然要使用(64位基址 + 32位变址)的方式来寻址,如果目标Klass不在一个连续的可控空间内(连续的4G[32位能标识的最大内存范围]以内的),那么就可能产生:某个Klass的地址 - 基址 > 32位的可表示的数值范围,这样就导致压缩指针无法表示目标地址,也就无法引用目标类了。

虽然可以认为这块空间是独立的,和元数据空间的其他数据是隔离的。但是两部分空间的committed内存还是要受到MaxMetaSpaceSize的限制。

如果关闭了指针压缩,那么就不会有这么一块连续的独立空间来存储这些Klass,这些Klass都是和non-Klass穿插混合存储在metasapce中的。

通过日志里的具体数值,可以知道Klass对象合计占用的的空间不过13M左右。但保留内存空间是1G,可以通过-XX:CompressedClassSpaceSize进行设置,默认是1G,jvm内部限制最大值3G。

那么问题来了,在前面分析过的CommandLine flags的参数中,CompressedClassSpaceSize是被动态调整到了512M的,为什么这里的reserved还是1G呢? 至于为什么,还是要回头来再看一下CompressedClassSpaceSize被修改为512M之前代码逻辑。

a)class space的保留内存是如何设置的?

源码片段 -> metaspace.cpp

void Metaspace::ergo_initialize() {
  
  // 省略了很多代码

  CompressedClassSpaceSize = align_size_down_bounded(CompressedClassSpaceSize, _reserve_alignment);
  // 通过调用set_compressed_class_space_size方法把CompressedClassSpaceSize当前的值拷贝了一份赋值给了_compressed_class_space_size变量。
  set_compressed_class_space_size(CompressedClassSpaceSize);ressedClassSpaceSize);

  // 省略了很多代码
}  

void Metaspace::allocate_metaspace_compressed_klass_ptrs(char* requested_addr, address cds_base) {
    // 省略了很多代码

    // compressed_class_space_size() 返回的就是_compressed_class_space_size
    ReservedSpace metaspace_rs = ReservedSpace(compressed_class_space_size(), _reserve_alignment,
    large_pages,requested_addr, 0);
    // 省略了很多代码

    // 初始化class space
    initialize_class_space(metaspace_rs);

    // 省略了很多代码
}

void Metaspace::initialize_class_space(ReservedSpace rs) {
   // 省略了很多代码
   // _class_space_list
  _class_space_list = new VirtualSpaceList(rs);
  // 省略了很多代码
}

_compressed_class_space_size存储的是1G。保留内存的开辟也是基于这个值申请的,所以最中体现到日志中的就是1G。

b)meta space的保留内存是如何设置的?

源码片段 -> metaspace.cpp

void Metaspace::global_initialize() {

    // 省略了很多代码

    // 这个方法的调用就是分配class space的内存(串联一下上面a部分中的源码)
    allocate_metaspace_compressed_klass_ptrs(base, 0);


    // 省略了很多代码

    // InitialBootClassLoaderMetaspaceSize前面介绍过,设置的是256M,
    // 所以:_first_chunk_word_size =  256M/8
    _first_chunk_word_size = InitialBootClassLoaderMetaspaceSize / BytesPerWord;
    // _first_chunk_word_size对齐
    _first_chunk_word_size = align_word_size_up(_first_chunk_word_size);
    
    // 省略了很多代码
    // word_size = 2 * 256M / 8
    size_t word_size = VIRTUALSPACEMULTIPLIER * _first_chunk_word_size;
    // _first_chunk_word_size对其为2的幂
    word_size = align_size_up(word_size, Metaspace::reserve_alignment_words());

    // Initialize the list of virtual spaces.
    // 初始化 meta space。word_size虽然值除以了8 但最终效果还是512M。之所以除以8,是为了和指针地址直接做数值运算
    _space_list = new VirtualSpaceList(word_size);
}

从上面的源码可以看到,_space_list的保留内存是512M。而当要打印整个meta space的保留内存时会调用以下代码

源码片段 -> metaspace.cpp

void MetaspaceAux::print_on(outputStream* out) {
  Metaspace::MetadataType nct = Metaspace::NonClassType;

  out->print_cr(" Metaspace       "
                "used "      SIZE_FORMAT "K, "
                "capacity "  SIZE_FORMAT "K, "
                "committed " SIZE_FORMAT "K, "
                "reserved "  SIZE_FORMAT "K",
                used_bytes()/K,
                capacity_bytes()/K,
                committed_bytes()/K,
                reserved_bytes()/K); // 重点在此处

  if (Metaspace::using_class_space()) {
    Metaspace::MetadataType ct = Metaspace::ClassType;
    out->print_cr("  class space    "
                  "used "      SIZE_FORMAT "K, "
                  "capacity "  SIZE_FORMAT "K, "
                  "committed " SIZE_FORMAT "K, "
                  "reserved "  SIZE_FORMAT "K",
                  used_bytes(ct)/K,
                  capacity_bytes(ct)/K,
                  committed_bytes(ct)/K,
                  reserved_bytes(ct)/K);
  }
}

class MetaspaceAux : AllStatic {
    // 这里就可以看出是将 _space_list 和 _class_space_list 的两部分保留内存相加
    static size_t reserved_bytes() {
        return reserved_bytes(Metaspace::ClassType) +
            reserved_bytes(Metaspace::NonClassType);
    }
}

结合a部分中_class_space_list的保留内存是1G。最终整个meta space的合计保留内存是512M + 1G = 1.5G。

2.2.2、gc执行的详细信息

0)GC的类型

2021-11-06T06:07:37.195+0800: 23577.357: [GC pause (G1 Evacuation Pause) (young), 0.1141917 secs]

2021年11月6日早晨6点07分37秒,距离jvm进程启动过去了23577.357秒(约6小时30分钟) [针对年轻代的数据迁移产生的停顿,花了0.114秒]
(young) 指的是年轻代gc,如果是 (mixed) 则指的是混合gc(年轻代+老年代)

(G1 Evacuation Pause) 指的是产生本次gc的原因,空间使用达到阈值,如果是young gc则意味着年轻代已满,如果是mixed gc则大多数情况下是老年代空间占整个堆空间比例达到阈值(默认45%)。这个地方还可能是 (G1 Humongous Allocation) 表示巨型对象空间分配申请。每一个巨型对象的的内存分配都会触发一次gc尝试,如果当前已经处在并发标记周期阶段了,则不会主动发起gc,否则会主动发起gc。所以应用程序开发一定要注意避免产生巨型对象。


GC动作中有很多细节步骤,这些步骤中有些是可以并行执行的,有些是必须串行执行的。但是无论并行还是串行,都可以是多个GC线程协同工作的。

1)并行执行的部分

[Parallel Time: 97.6 ms, GC Workers: 4]

多线程并行回收,一共4个工作线程,花了83.8毫秒


[GC Worker Start (ms): Min: 23577359.3, Avg: 23577359.3, Max: 23577359.4, Diff: 0.1]

4个回收线程的启动时间(这是数值指的是jvm进程启动时间点 - 当前时间点 的毫秒时间差)
min就是最早启动的,max就是最晚启动的,avg就是平均的启动时间,diff = max-min


[Ext Root Scanning (ms): Min: 3.4, Avg: 4.5, Max: 7.6, Diff: 4.2, Sum: 18.0]

外部根扫描(堆外扫描):外部根包括(JVM系统目录、VM数据结构、JNI线程句柄、硬件寄存器、全局变量、线程堆栈根)


[Update RS (ms): Min: 12.0, Avg: 14.7, Max: 15.7, Diff: 3.6, Sum: 58.9]

更新RSet:每个region都维护一个Rset,在有外部region引用本region内对象时,会记录外部region的card table索引。
但这个引用信息的变更,是同步先写到一个队列,然后由一个单独的线程(叫做:并发优化线程)读取队列并异步更新RSet。
所以,就可能存在这样的情况,垃圾回收时,队列中还存在着未被处理的变更日志,那么RSet信息就是不完整的。所以这个阶段要STW,检查队列中是否还有未处理完的变更记录,补充更新到RSet。


[Processed Buffers: Min: 32, Avg: 69.8, Max: 98, Diff: 66, Sum: 279]

已处理的缓冲:指的就是队列中那部分还没被消费完的变更记录。


[Scan RS (ms): Min: 0.4, Avg: 0.5, Max: 0.5, Diff: 0.0, Sum: 1.9]

扫描所有region的RSet,来确定自己内部的存活对象。


[Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]

代码根扫描:扫描的是被JIT即时编译器编译后的代码对堆的region内部对象的引用情况。


[Object Copy (ms): Min: 77.2, Avg: 77.6, Max: 77.8, Diff: 0.6, Sum: 310.2]

对象迁移:这一步就是将存活对象迁移到新的region和survivor,也有一部分会晋升到老年代region。回收老的region。


[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

GC终止耗时:工作线程终止


[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]

终止尝试次数:1次


[GC Worker Other (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.4]

该部分并非GC的活动,而是JVM的活动导致占用了GC暂停时间(例如JNI编译)


[GC Worker Total (ms): Min: 97.4, Avg: 97.4, Max: 97.5, Diff: 0.1, Sum: 389.8]

4个回收线程总执行耗时


[GC Worker End (ms): Min: 23577456.8, Avg: 23577456.8, Max: 23577456.8, Diff: 0.1]

4个回收线程的结束时间(这是数值指的是jvm进程启动时间点 - 当前时间点 的毫秒时间差)


2)串行执行的部分

[Code Root Fixup: 0.0 ms]

代码根修正:代码根所引用的存活对象,从一个region迁移到了另外的region,那么就需要更新他对这个对象的引用地址


[Code Root Purge: 0.0 ms]

代码根清理:清理代码根中不再被使用的代码


[Clear CT: 0.8 ms]

清理card table:清理全局卡表中的已扫描标志


[Other: 15.8 ms]

其他操作耗时


[Choose CSet: 0.0 ms]

选择要回收的集合(只有在混合回收时,这一步才有意义,需要基于停顿时间的考量,利用启发性算法重新界定需要回收哪些region。单纯的young gc,就是所有年轻代region)


[Ref Proc: 9.0 ms]

引用处理:针对软引用(内存不足才回收)、弱引用(发生gc就可回收)、虚引用、final引用、JNI引用


[Ref Enq: 0.0 ms]

可以被回收的引用入队。


[Redirty Cards: 0.2 ms]

上一步的引用处理,会更新RSet信息,所以也需要同步标记全局开标中对应的card table为脏卡片


[Humongous Register: 1.5 ms]

巨型对象注册(统计):虽然巨型对象是存储在老年代分区的,但是G1内部也做了特殊优化手段,在young gc是也会顺带回收一些巨型对象分区。在young gc时,会基于region的RSet确定外部对自己的引用。

而一旦Rset所对应的region是一个Humongous region(以下简称:H-Region),又通过逐层引用排查发现该H-Region已经不再被引用。那么就意味着这个H-Region可被回收。


[Humongous Reclaim: 0.1 ms]

回收H-Region的耗时


[Free CSet: 3.7 ms]

回收CSet中region的空间,并将这些region重新置为空闲。


[Eden: 2366.0M(2366.0M)->0.0B(2372.0M) Survivors: 92160.0K->86016.0K Heap: 3410.9M(4096.0M)->1055.1M(4096.0M)]

伊甸区:回收前总内存一共2366M,使用了2366M。回收后总内存增长了6M,变为2372M,但是使用内存变为OM,这是因为已经都被回收。

幸存区:回收前
内存使用90M,回收后内存使用84M。

整个堆空间:总内存一直都是4G,回收前使用了3410M,回收后使用了1055M。差值是2355M,对比伊甸区被清空的2366M和幸存区减少的8M,这中间有19M的差值。可以估算有19M的数据晋升到了老年代。


2.2.3、gc结束之后堆和元数据信息

Heap after GC invocations=303 (full 0):
 garbage-first heap   total 4194304K, used 1080394K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 42 young (86016K), 42 survivors (86016K)
 Metaspace       used 108788K, capacity 351624K, committed 353612K, reserved 1572864K
  class space    used 12362K, capacity 12998K, committed 13388K, reserved 1048576K

这部分信息的含义在2.2.1中已经详细介绍过,这里重点要比对的是和2.2.1中gc开始之前的一些数据的差异。

1)回收次数

invocations:gc之前是302、这里(gc之后)是303

2)堆的内存使用情况

差异只是used的值,gc之前是3.3G,这里是1055M(1G多一点)。
重点留意中括号的中间的数值0x00000006e0204000,gc前后是没有变化的。至于为何没有变化,在2.2.1中详细分析过这个数值的由来。

4)元数据空间使用情况

元数据空间的各项统计指标都和gc之前没有差异。

2.3、mixed gc日志

前文已经介绍过mixed gc不是一个gc动作,是一套动作,包含多个步骤,多次gc。下面来看一个完整的mixed gc的日志样例。

{Heap before GC invocations=439 (full 0):
 garbage-first heap   total 4194304K, used 3719960K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 895 young (1832960K), 25 survivors (51200K)
 Metaspace       used 115155K, capacity 358590K, committed 358860K, reserved 1572864K
  class space    used 12989K, capacity 13737K, committed 13772K, reserved 1048576K
2021-11-06T14:24:22.132+0800: 53382.293: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0597538 secs]
   [Parallel Time: 51.9 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 53382297.2, Avg: 53382297.2, Max: 53382297.2, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 5.2, Avg: 5.5, Max: 5.7, Diff: 0.5, Sum: 22.0]
      [Update RS (ms): Min: 7.1, Avg: 7.2, Max: 7.2, Diff: 0.1, Sum: 28.8]
         [Processed Buffers: Min: 52, Avg: 57.8, Max: 67, Diff: 15, Sum: 231]
      [Scan RS (ms): Min: 0.2, Avg: 0.3, Max: 0.3, Diff: 0.0, Sum: 1.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 37.5, Avg: 37.8, Max: 38.1, Diff: 0.6, Sum: 151.1]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
      [GC Worker Other (ms): Min: 0.1, Avg: 0.1, Max: 0.3, Diff: 0.2, Sum: 0.5]
      [GC Worker Total (ms): Min: 50.8, Avg: 50.8, Max: 50.9, Diff: 0.1, Sum: 203.4]
      [GC Worker End (ms): Min: 53382348.0, Avg: 53382348.1, Max: 53382348.1, Diff: 0.1]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.3 ms]
   [Other: 7.5 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 1.9 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.1 ms]
      [Free CSet: 2.2 ms]
   [Eden: 1740.0M(1740.0M)->0.0B(1736.0M) Survivors: 51200.0K->51200.0K Heap: 3632.8M(4096.0M)->1896.8M(4096.0M)]
Heap after GC invocations=440 (full 0):
 garbage-first heap   total 4194304K, used 1942303K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 25 young (51200K), 25 survivors (51200K)
 Metaspace       used 115155K, capacity 358590K, committed 358860K, reserved 1572864K
  class space    used 12989K, capacity 13737K, committed 13772K, reserved 1048576K
}
 [Times: user=0.16 sys=0.04, real=0.06 secs] 
2021-11-06T14:24:22.192+0800: 53382.353: [GC concurrent-root-region-scan-start]
2021-11-06T14:24:22.249+0800: 53382.410: [GC concurrent-root-region-scan-end, 0.0567419 secs]
2021-11-06T14:24:22.249+0800: 53382.410: [GC concurrent-mark-start]
2021-11-06T14:24:22.776+0800: 53382.938: [GC concurrent-mark-end, 0.5277478 secs]
2021-11-06T14:24:22.778+0800: 53382.939: [GC remark 2021-11-06T14:24:22.778+0800: 53382.939: [Finalize Marking, 0.0004157 secs] 2021-11-06T14:24:22.778+0800: 53382.940: [GC ref-proc, 1.2651008 secs] 2021-11-06T14:24:24.043+0800: 53384.205: [Unloading, 0.0628071 secs], 1.3315890 secs]
 [Times: user=1.33 sys=0.19, real=1.33 secs] 
2021-11-06T14:24:24.111+0800: 53384.272: [GC cleanup 1908M->1577M(4096M), 0.0058486 secs]
 [Times: user=0.02 sys=0.00, real=0.01 secs] 
2021-11-06T14:24:24.118+0800: 53384.279: [GC concurrent-cleanup-start]
2021-11-06T14:24:24.119+0800: 53384.280: [GC concurrent-cleanup-end, 0.0009447 secs]
{Heap before GC invocations=441 (full 0):
 garbage-first heap   total 4194304K, used 3380949K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 893 young (1828864K), 25 survivors (51200K)
 Metaspace       used 115106K, capacity 358512K, committed 358860K, reserved 1572864K
  class space    used 12982K, capacity 13724K, committed 13772K, reserved 1048576K
2021-11-06T14:26:53.067+0800: 53533.229: [GC pause (G1 Evacuation Pause) (young), 0.1490704 secs]
   [Parallel Time: 60.2 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 53533231.3, Avg: 53533231.4, Max: 53533231.4, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 4.3, Avg: 5.2, Max: 7.3, Diff: 2.9, Sum: 20.8]
      [Update RS (ms): Min: 22.6, Avg: 24.2, Max: 25.1, Diff: 2.6, Sum: 96.9]
         [Processed Buffers: Min: 54, Avg: 69.0, Max: 104, Diff: 50, Sum: 276]
      [Scan RS (ms): Min: 0.0, Avg: 0.3, Max: 0.4, Diff: 0.4, Sum: 1.1]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 20.1, Avg: 21.5, Max: 22.7, Diff: 2.6, Sum: 86.1]
      [Termination (ms): Min: 7.3, Avg: 8.6, Max: 10.4, Diff: 3.1, Sum: 34.5]
         [Termination Attempts: Min: 455, Avg: 509.0, Max: 567, Diff: 112, Sum: 2036]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.4]
      [GC Worker Total (ms): Min: 59.9, Avg: 60.0, Max: 60.1, Diff: 0.3, Sum: 239.9]
      [GC Worker End (ms): Min: 53533291.3, Avg: 53533291.3, Max: 53533291.4, Diff: 0.1]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.3 ms]
   [Other: 88.5 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 82.9 ms]
      [Ref Enq: 0.1 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 1.5 ms]
      [Humongous Reclaim: 0.1 ms]
      [Free CSet: 2.3 ms]
   [Eden: 1736.0M(1736.0M)->0.0B(154.0M) Survivors: 51200.0K->51200.0K Heap: 3301.7M(4096.0M)->1570.4M(4096.0M)]
Heap after GC invocations=442 (full 0):
 garbage-first heap   total 4194304K, used 1608107K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 25 young (51200K), 25 survivors (51200K)
 Metaspace       used 115106K, capacity 358512K, committed 358860K, reserved 1572864K
  class space    used 12982K, capacity 13724K, committed 13772K, reserved 1048576K
}
 [Times: user=0.26 sys=0.07, real=0.14 secs] 
{Heap before GC invocations=442 (full 0):
 garbage-first heap   total 4194304K, used 1765803K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 102 young (208896K), 25 survivors (51200K)
 Metaspace       used 115106K, capacity 358512K, committed 358860K, reserved 1572864K
  class space    used 12982K, capacity 13724K, committed 13772K, reserved 1048576K
2021-11-06T14:27:05.068+0800: 53545.229: [GC pause (G1 Evacuation Pause) (mixed), 0.0407992 secs]
   [Parallel Time: 35.3 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 53545230.0, Avg: 53545230.1, Max: 53545230.1, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 2.1, Avg: 2.5, Max: 3.3, Diff: 1.3, Sum: 10.0]
      [Update RS (ms): Min: 4.0, Avg: 4.4, Max: 4.6, Diff: 0.6, Sum: 17.8]
         [Processed Buffers: Min: 32, Avg: 48.2, Max: 59, Diff: 27, Sum: 193]
      [Scan RS (ms): Min: 2.8, Avg: 3.1, Max: 3.4, Diff: 0.6, Sum: 12.4]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.5, Sum: 1.1]
      [Object Copy (ms): Min: 24.5, Avg: 24.9, Max: 25.1, Diff: 0.6, Sum: 99.5]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 7]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 35.2, Avg: 35.2, Max: 35.3, Diff: 0.1, Sum: 140.9]
      [GC Worker End (ms): Min: 53545265.3, Avg: 53545265.3, Max: 53545265.3, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.3 ms]
   [Other: 5.2 ms]
      [Choose CSet: 0.4 ms]
      [Ref Proc: 1.8 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.2 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 1.8 ms]
   [Eden: 154.0M(154.0M)->0.0B(188.0M) Survivors: 51200.0K->16384.0K Heap: 1724.4M(4096.0M)->1145.9M(4096.0M)]
Heap after GC invocations=443 (full 0):
 garbage-first heap   total 4194304K, used 1173387K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 8 young (16384K), 8 survivors (16384K)
 Metaspace       used 115106K, capacity 358512K, committed 358860K, reserved 1572864K
  class space    used 12982K, capacity 13724K, committed 13772K, reserved 1048576K
}
 [Times: user=0.13 sys=0.02, real=0.04 secs] 
{Heap before GC invocations=443 (full 0):
 garbage-first heap   total 4194304K, used 1365899K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 102 young (208896K), 8 survivors (16384K)
 Metaspace       used 115106K, capacity 358512K, committed 358860K, reserved 1572864K
  class space    used 12982K, capacity 13724K, committed 13772K, reserved 1048576K
2021-11-06T14:27:20.626+0800: 53560.788: [GC pause (G1 Evacuation Pause) (mixed), 0.1076664 secs]
   [Parallel Time: 101.8 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 53560789.1, Avg: 53560789.1, Max: 53560789.1, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 2.2, Avg: 2.5, Max: 2.7, Diff: 0.5, Sum: 9.8]
      [Update RS (ms): Min: 15.1, Avg: 15.1, Max: 15.1, Diff: 0.0, Sum: 60.5]
         [Processed Buffers: Min: 57, Avg: 70.8, Max: 85, Diff: 28, Sum: 283]
      [Scan RS (ms): Min: 11.7, Avg: 12.3, Max: 12.9, Diff: 1.3, Sum: 49.1]
      [Code Root Scanning (ms): Min: 0.4, Avg: 1.0, Max: 1.5, Diff: 1.1, Sum: 4.0]
      [Object Copy (ms): Min: 65.4, Avg: 66.1, Max: 66.9, Diff: 1.4, Sum: 264.6]
      [Termination (ms): Min: 4.3, Avg: 4.7, Max: 5.3, Diff: 1.1, Sum: 18.8]
         [Termination Attempts: Min: 408, Avg: 490.0, Max: 617, Diff: 209, Sum: 1960]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 101.6, Avg: 101.7, Max: 101.7, Diff: 0.1, Sum: 406.7]
      [GC Worker End (ms): Min: 53560890.7, Avg: 53560890.8, Max: 53560890.8, Diff: 0.0]
   [Code Root Fixup: 0.1 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.4 ms]
   [Other: 5.4 ms]
      [Choose CSet: 0.5 ms]
      [Ref Proc: 1.7 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.4 ms]
      [Humongous Register: 0.2 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 1.7 ms]
   [Eden: 188.0M(188.0M)->0.0B(2440.0M) Survivors: 16384.0K->16384.0K Heap: 1333.9M(4096.0M)->859.0M(4096.0M)]
Heap after GC invocations=444 (full 0):
 garbage-first heap   total 4194304K, used 879616K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 8 young (16384K), 8 survivors (16384K)
 Metaspace       used 115106K, capacity 358512K, committed 358860K, reserved 1572864K
  class space    used 12982K, capacity 13724K, committed 13772K, reserved 1048576K
}
 [Times: user=0.36 sys=0.06, real=0.10 secs] 

为了便于从宏观上观察整个mixed gc的步骤,我们把gc pause类型日志内的一些细节内容省略掉,因为内部的具体内容格式是和我们前面介绍过的young gc的格式是一致的。
精简后的内容如下:

{
Heap before GC...    
2021-11-06T14:24:22.132+0800: 53382.293: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0597538 secs]
...
[Eden: 1740.0M(1740.0M)->0.0B(1736.0M) Survivors: 51200.0K->51200.0K Heap: 3632.8M(4096.0M)->1896.8M(4096.0M)]
Heap after GC invocations=440 (full 0):
 garbage-first heap   total 4194304K, used 1942303K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 25 young (51200K), 25 survivors (51200K)
  ...  
}
 [Times: user=0.16 sys=0.04, real=0.06 secs] 
 
// 华丽分割线:这是一个young gc,但是重点是后面标记了initial-mark,说明这是一个并发标记周期的开始。

2021-11-06T14:24:22.192+0800: 53382.353: [GC concurrent-root-region-scan-start]
2021-11-06T14:24:22.249+0800: 53382.410: [GC concurrent-root-region-scan-end, 0.0567419 secs]
2021-11-06T14:24:22.249+0800: 53382.410: [GC concurrent-mark-start]
2021-11-06T14:24:22.776+0800: 53382.938: [GC concurrent-mark-end, 0.5277478 secs]
2021-11-06T14:24:22.778+0800: 53382.939: [GC remark 2021-11-06T14:24:22.778+0800: 53382.939: [Finalize Marking, 0.0004157 secs] 2021-11-06T14:24:22.778+0800: 53382.940: [GC ref-proc, 1.2651008 secs] 2021-11-06T14:24:24.043+0800: 53384.205: [Unloading, 0.0628071 secs], 1.3315890 secs]
 [Times: user=1.33 sys=0.19, real=1.33 secs] 
2021-11-06T14:24:24.111+0800: 53384.272: [GC cleanup 1908M->1577M(4096M), 0.0058486 secs]
 [Times: user=0.02 sys=0.00, real=0.01 secs] 
2021-11-06T14:24:24.118+0800: 53384.279: [GC concurrent-cleanup-start]
2021-11-06T14:24:24.119+0800: 53384.280: [GC concurrent-cleanup-end, 0.0009447 secs]

// 华丽分割线:这里的步骤就是并发标记、重新标记、并发清理了。 到这,也表示一个并发标记周期结束了。

{
Heap before GC...    
2021-11-06T14:26:53.067+0800: 53533.229: [GC pause (G1 Evacuation Pause) (young), 0.1490704 secs]
...
[Eden: 1736.0M(1736.0M)->0.0B(154.0M) Survivors: 51200.0K->51200.0K Heap: 3301.7M(4096.0M)->1570.4M(4096.0M)]
Heap after GC invocations=442 (full 0):
 garbage-first heap   total 4194304K, used 1608107K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 25 young (51200K), 25 survivors (51200K)
 ...  
}
 [Times: user=0.36 sys=0.06, real=0.10 secs]
 
// 华丽分割线:这是一次单纯的young gc,只清理了年轻代。

{
Heap before GC...    
2021-11-06T14:27:05.068+0800: 53545.229: [GC pause (G1 Evacuation Pause) (mixed), 0.0407992 secs]
...
[Eden: 154.0M(154.0M)->0.0B(188.0M) Survivors: 51200.0K->16384.0K Heap: 1724.4M(4096.0M)->1145.9M(4096.0M)]
Heap after GC invocations=443 (full 0):
 garbage-first heap   total 4194304K, used 1173387K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 8 young (16384K), 8 survivors (16384K)
 ...  
}
 [Times: user=0.36 sys=0.06, real=0.10 secs]
 
// 华丽分割线:这是一次混合gc,年轻代和老年代都被清理

{
Heap before GC...    
2021-11-06T14:27:20.626+0800: 53560.788: [GC pause (G1 Evacuation Pause) (mixed), 0.1076664 secs]
...
[Eden: 188.0M(188.0M)->0.0B(2440.0M) Survivors: 16384.0K->16384.0K Heap: 1333.9M(4096.0M)->859.0M(4096.0M)]
Heap after GC invocations=444 (full 0):
 garbage-first heap   total 4194304K, used 879616K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 8 young (16384K), 8 survivors (16384K)
... 
}
 [Times: user=0.36 sys=0.06, real=0.10 secs] 
 
// 华丽分割线:这也是一次混合gc,年轻代和老年代都被清理。 

// 再往下的日志就是一次普通的young gc了。所以认为上面的mixed gc 日志结束,就是一个完整的mixed回收的结束了。

一个完整的 mixed gc过程 是从 (young) (initial-mark) 开始,到 (mixed) 结束。
具体到哪一条(mixed)结束呢,从(initial-mark)往后找,找到的第一个GC pause是mixed类型的日志,如果这个mixed之后的GC pause日志不是mixed类型了,说明这个mixed日志就是这个 mixed gc过程 的结束。
整个mixed gc过程由几部分组成:并发标记周期(1个或者N个)+ 普通young gc(0次或者多次) + 混合回收周期

2.3.1 并发标记周期

(young) (initial-mark) 开始到 GC concurrent-cleanup-end 结束

1、initial-mark 初始标记:

GC pause (G1 Evacuation Pause) (young) (initial-mark)
并发标记周期的开始步骤,一定是伴随着一次young gc一起产生。初始标记做的事情是标记根节点。这一步是需要STW的,正好可以和young gc一起做了,因为young gc也是STW,等于是搭个顺风车。


2、concurrent-root-region 根分区扫描:

初始标记完成之后,也就意味着完成了一个young gc,存活的对象都被转移到了survivor分区。那么这些survivor分区的对象就是存活对象,他们都会被标记为根对象,这个过程称为根分区扫描,实际上扫描的是survivor分区。这个过程因为扫描的是survivor分区,所以这个过程不能和young gc同时进行,因为young gc会造成survivor分区的变化。


3、Concurrent Marking 并发标记:

并发标记和应用线程并发执行。这一过程会扫描老年代,标记可达对象。


4、GC remark 重新标记:

STW的。这一过程会处理引用变更日志缓冲区的未被处理的日志。同时,[GC ref-proc]引用处理也是重新标记阶段的一部分。这一步是可以多线程并行执行。


5、GC cleanup 独占清理:

紧跟重新标记,此时还是STW的,同时这一步还会做三个事情:

  • RSet梳理:启发式算法或根据Rset尺寸对分区定义不同的等级。(RSet尺寸越大,说明对本分区的引用越多,那么本分区的等级也会越高)
  • 堆分区梳理:针对老年代的分区,基于释放空间和暂停目标,定识别出回收收益高的region。
  • 识别所有空闲分区:标记那些内部对象都是非存活的分区。

6、GC concurrent-cleanup 并发清理:

回收线程和应用程序并发执行,回收上一步被标记为空闲的分区。


2.3.2、mixed(混合)回收周期

在上面介绍的并发标记周期中,虽然也经历了回收(清理)动作(GC concurrent-cleanup),但是只是仅仅清理了那些对象都不存活的空闲分区。对于那些部分对象存活的年轻代和老年代分区还没有进行数据的迁移和分区的清理动作。但是在并发标记周期中已经识别出了应该回收哪些分区,所以接下来都是gc pause动作,会包含一次或多次的GC pause。最后一次的GC pause是mixed类型。混合回收周期内,也是可以发生单纯的young gc的。

我们上面的的例子里,混合回收周期内就包含了一次young gc + 两次mixed gc。

{
Heap before GC...    
2021-11-06T14:26:53.067+0800: 53533.229: [GC pause (G1 Evacuation Pause) (young), 0.1490704 secs]
...
[Eden: 1736.0M(1736.0M)->0.0B(154.0M) Survivors: 51200.0K->51200.0K Heap: 3301.7M(4096.0M)->1570.4M(4096.0M)]
Heap after GC invocations=442 (full 0):
 garbage-first heap   total 4194304K, used 1608107K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 25 young (51200K), 25 survivors (51200K)
 ...  
}
 [Times: user=0.36 sys=0.06, real=0.10 secs]

这是一次单纯的young gc,只清理了年轻代。堆空间由3301M减少到1570M。

{
Heap before GC...    
2021-11-06T14:27:05.068+0800: 53545.229: [GC pause (G1 Evacuation Pause) (mixed), 0.0407992 secs]
...
[Eden: 154.0M(154.0M)->0.0B(188.0M) Survivors: 51200.0K->16384.0K Heap: 1724.4M(4096.0M)->1145.9M(4096.0M)]
Heap after GC invocations=443 (full 0):
 garbage-first heap   total 4194304K, used 1173387K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 8 young (16384K), 8 survivors (16384K)
 ...  
}
 [Times: user=0.36 sys=0.06, real=0.10 secs]

这是一次混合收集,年轻代和老年代都被清理,但年轻代被清理前仅仅使用了154M,清理后整个堆空间从1724M减少到1145M,我们大约可估算,即使年轻代都被清理堆空间大概减少到1570M,而当前是1145M,所以老年代被清理释放的空间应该>=425M。

{
Heap before GC...    
2021-11-06T14:27:20.626+0800: 53560.788: [GC pause (G1 Evacuation Pause) (mixed), 0.1076664 secs]
...
[Eden: 188.0M(188.0M)->0.0B(2440.0M) Survivors: 16384.0K->16384.0K Heap: 1333.9M(4096.0M)->859.0M(4096.0M)]
Heap after GC invocations=444 (full 0):
 garbage-first heap   total 4194304K, used 879616K [0x00000006e0000000, 0x00000006e0204000, 0x00000007e0000000)
  region size 2048K, 8 young (16384K), 8 survivors (16384K)
... 
}
 [Times: user=0.36 sys=0.06, real=0.10 secs] 

这是第二次混合收集,年轻代和老年代都被清理,年轻代被清理前仅仅使用了188M,清理后整个堆空间从1334M减少到859M,老年代至少被清理了287M。

3、参考资料

https://www.cnblogs.com/bjkandy/p/13877598.html
https://www.shangmayuan.com/a/d396bf8d18d94696a426a1ba.html
https://blog.csdn.net/zhanggang807/article/details/46011341

你可能感兴趣的:(jdk,java技术,源码解析,java,jvm,GC日志,G1,G1收集器)