JVM Garbage Collection

摘要: gc

JVM 垃圾回收

Jvm 内存结构

运行时数据区

程序运行期间会使用到的运行时数据区

PC寄存器

每一个虚拟机线程都有自己的PC寄存器,保存Java虚拟机正在执行的字节码指令的地址,如果该方法是 native 的,那 PC 寄存器的值是 undefined

Java 虚拟机栈

线程私有的,栈与线程同时创建,生命周期和线程是一样的。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧 ,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从开始调用到执行完成都对应着一个栈帧在虚拟机栈中入栈到出栈的过程

栈帧(Frame)

用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派

栈帧随着方法调用而创建,随着方法结束而销毁,无论方法是正常完成还是异常完成

栈帧的存储空间分配在Java虚拟机栈

每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用

栈帧容量的大小仅仅取决于Java虚拟机的实现和方法调用时可被分配的内存

一条线程只有正在执行某个方法的栈帧是活动的,叫做当前栈帧,对应的方法叫当前方法,定义这个方法的类叫当前类。对局部变量表和操作数栈的各种操作,通常指的是当前栈帧进行的操作

栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一条线程的栈帧

如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。当一个新的方法被调用,则会新建一个栈帧并成为当前栈帧,当方法返回时会将结果(当前新的栈帧)返回给上一个栈帧,当前栈帧丢弃,上一个栈帧重新成为当前栈帧。

  • 局部变量表

长度由编译期决定,存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用

  • 一个局部变量可以保存类型boolean、byte、char、short、float、reference和returnAddress的数据
  • 两个局部变量可以保存类型为long和double的数据

当方法被调用时候,参数将会传递至从0开始的连续的局部变量表里。如果是实例方法被调用则第0个局部变量一定是this

局部变量使用索引来进行定位访问,0-max

long和double这种需要两个局部变量的类型,索引取最小的那个局部变量。

  • 操作数栈

同局部变量表,长度由编译期决定,存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用

操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。

Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果,例子参考初识jvm指令执行流程

一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度

  • 动态链接

在Class文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用

类加载的过程中将要解析掉尚未被解析的符号引用,并且将变量访问转化为访问这些变量的存储结构所在的运行时内存位置的正确偏移量

由于动态链接的存在,通过晚期绑定(Late Binding)使用的其他类的方法和变量在发生变化时,将不会对调用它们的方法构成影响

Java堆

可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域

Java堆在虚拟机启动的时候就被创建,它存储了被自动内存管理系统所管理的各种对象,这些受管理的对象无需,也无法显式地被销毁

方法区

可供各条线程共享的运行时内存区域

方法区在虚拟机启动的时候被创建,存储了每一个类的结构信息,例如运行时常量池(存放编译器生成的各种字面量和符号引用)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集,在 JDK1.7 中是 Perm Space , 在 JDK1.8 中是Meta Space

运行时常量池

每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用

每一个运行时常量池都分配在Java虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来

本地方法栈

如果支持本地方法栈,则会在线程创建的时候按线程分配

垃圾回收算法

JVM 中内存结构图

img

堆区 又被划分为 新生代(Young) 和 老年代(Old)

Young 新生代,又被划分为 Eden区S区 ,默认情况下JVM采取的是一种动态分配的策略(-XX:+UsePSAdaptiveSurvivorSizePolicy),根据对象生成的速率,以及S区使用情况动态调整Eden区和S区的比例,也可以使用参数固定此比例(-XX:SurvivorRatio) ,比例越低浪费的堆空间就越高(因为S区有一个区域一直为空,S区大了就会浪费一部分空间),至于为什么会有S0与S1,可以参考下面的可达性分析清除方式,这里不过多解释

Metaspace 存放 Class 、Package 、Method 、Field 、字节码 、常量池 、符号引用等等

CCS 压缩类空间,只有启用压缩类短指针的时候才会存在。出现的原因:在堆里面分配每一个对象都会有一个指向自己 Class 的指针,64位的虚拟机每个指针长度是64位的,考虑性能原因我们可以把这个指针使用短指针来引用,使用32位的指针,如果使用短指针,其所引用的Class文件则会存放到CCS区,在 JVM 中是默认开启的 UseCompressedClassPointers ,默认1G大小,可以使用 CompressedClassSpaceSize 设置大小

CodeCache 存放JIT即时编译代码 、Native代码,可以使用 -XX:InitialCodeCacheSize -XX:ReservedCodeCacheSize 设置初始和最大大小

需要注意的是在我们调用 new 指令时,它会在Eden区划出一块作为存储对象的内存,由于堆空间是共享的(参考上文中 Jvm的内存结构),所以在划空间时候是需要进行同步的,JVM的解决办法是为每一个线程分配一段连续的内存作为线程私有的TLAB(Thread Local Allocation Buffer) ,并且只允许该线程拥有该部分内存,该技术对应参数(-XX:+UseTLAB ,默认开启)。

同样在线程申请内存时候需要加锁,线程主要维护两个指针,一个指向TLAB空余内存的起始Adress,一个指向TLAB末尾。而new的时候便可以通过 bump the pointer 来实现,即将第一个指向起始位置的指针加上请求的字节数,当加上字节数后的值大于指向末尾的指针的值,则当前线程重新申请新的TLAB。当Eden区耗尽则会触发minor GC

分析垃圾的方法

无法被程序引用的在堆上已分配的内存空间称为垃圾

古老的引用计数法

引用计数方式会为每个已分配的内存单元设置计数器,当计数器减少为0的时候意味着再无法被引用,将立即执行释放内存的动作。

引用计数 使用弱引用来解决循环引用带来的问题,弱引用不会影响计数器状态的引用,即使循环引用也不会阻止其被清除

  • 问题
  1. 如果分配的内存单元原本就很小,这个时候计数器所占的内存占比就比较大。而垃圾回收方式只需要设置一个标记位置而已
  2. 维护计数器的状态会占据整个程序执行时间中明显的部分,相当于将垃圾回收的消耗均摊到程序运行的整个过程
  3. 计数器代码分布在用户代码各处不易维护
  4. 当存在循环依赖时候内存释放比较复杂

主流的可达性分析法

基本思想是 标记-清除(mark-and-sweep) ,每隔一段时间或堆空间不足时候才进行一次垃圾回收,每次回收先将所有堆上分配的内存单元标记为 “不可到达” ,然后根据 GC Root 开始扫描,把可达到的内存单元标记为 “可以到达” 。最后回收标记为 “不可到达” 的内存单元。

GC Root: 类加载器、已启动且未停止的Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量

标记-清除 并不会受循环引用的影响,如A与B循环依赖 但是当AB都 不可达 时则会将AB都进行回收。标记-清除垃圾回收同样也提供了弱引用,原因是为了解决可能是人为造成的内存泄露(无意长时间持有了对已经不需要的对象的引用,如private static修饰的类变量)

  • 问题
  1. 在执行垃圾回收时候一般要把整个程序停下来并执行 标记-清除 的过程(不考虑并行回收) ,如果是实时系统一般无法承受这种因为标记过程过长而导致的时间消耗,其宁可选择引用计数均摊到整个运行过程中,分代式的垃圾回收一定程度上缓解了这个问题(例如java 虚拟机新生代与老年代的回收) ,但并没有根除消耗高峰的问题
  2. 最需要垃圾回收时(堆内存已经快分配尽了,但已分配也就是需要标记的有很多),它运行效果却最差(需要大量时间来标记,实际释放的却未必很多)
  3. 标记-清除 有两种实现思路,保守式准确式保守式 不需要知道内存的具体布局形式,会把所有看上去像指针的数值看作指针并纳入标记计算中; 准确式 则要求运行时系统清楚的了解内存的布局,能够分辨指针并只标记指针。前者不够准确,后者则需要消耗更多的内存和时间
  • 清除的方式
  1. 直接清除 把死亡对象所占据的内存标记为空闲内存,新建对象需要内存则从空闲内存中划分

    缺点: 非连续内存空间分配效率低,并且会造成内存碎片,体现在现实中就是内存不够分配提前GC

  2. 清除完进行压缩 在清除完将存活对象聚集到内存区域的起始位置,保证内存空间是连续的

    缺点: 压缩算法的开销

  3. 清除完进行复制再进行清理 首先把内存区域分为两部分,用指针from来维护之前需要分配内存的内存区域,在清除完将存活对象复制到to指针维护的内 存区域中,然后清理掉from区,最后重新交换from指针与to指针的内容

    缺点: 内存空间使用效率极低,只有50%

    img

现代垃圾回收器会综合上述几种清除方式,根据不同场景选出最合适的方式,例如JVM中会频繁进行Eden区回收,此时则采用复制方式,这样的原因是理想情况下清除垃圾后Eden区的对象基本都死亡了,需要复制的数据很少,使用复制算法效果很好。

存在的问题

如果老年代中的对象引用新生代的对象,在使用可达性分析标记存活对象的时候我们就需要扫描老年代。为了不做耗时较久的全堆扫描,HotSpot 的解决方案是 Card Table ,该技术将堆划分为一个个大小为512字节的 Card ,然后维护了一个 Table ,用来存储每张 Card 的一个标识位,这个标识位代表是否可能存在指向新生代对象的引用,若存在则认为这是一个脏的 Card 。则在新生代 Minor GC 时则不需要扫描整个老年代,而是寻找脏的 Card 将其中的对象加入到 Minor GC 的 GC Root 中,完成扫描则将所有标识位清空

对象分配

  1. 对象优先在Eden区分配
  2. 大对象直接进入老年代,可以通过参数( -XX:PretenureSizeThreshold )设置大对象的临界值,默认0,表示没有最大值
  3. 每次S区进行复制算法的时候都会被记录下来,如果一个对象复制次数为 15 (可通过参数 -XX:MaxTenuringThreshold 设置 ),则会进入老年代
  4. 在第3条的基础上会和 S 区存活对象在50%比例下(可通过参数 -XX:TargetSurvivorRatio 设置比例)的平均复制次数取一个最小值,达到这个值则会进入老年代

可通过参数 (-XX:+PrintTenuringDistribution) 打印 Minor GC 后存活对象的年龄分布情况

垃圾收集器

串行收集器Serial

开启参数

1
-XX:+UseSerialGC -XX:+UseSerialOldGC

并行收集器Parallel

多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

开启参数

1
2
-XX:+UseParallelGC -XX:+UseParallelOldGC
-XX:ParallelGCThreads=<N> # 多少GC线程 默认CPU>8 N=5/8 CPU<8 N=CPU

自适应调优

  • -XX:MaxGCPauseMillis= 最大停顿时间
  • -XX:GCTimeRatio= 吞吐量(花在应用时间和花在垃圾收集的时间的占比) 默认99,垃圾收集时间=1/1+N
  • -Xmx= 最大堆的大小

JVM会自动调整堆分区的大小来满足以上三个参数条件,当不够了就会变大,变化的比例同样可以通过JVM参数设置:

  • -XX:YoungGenerationSizeIncrement=<Y> 年轻代适应下大小百分比,默认值20
  • -XX:TenuredGenerationSizeIncrement=<T> 老年代适应下大小百分比,默认值20
  • -XX:AdaptiveSizeDecrementScaleFactor=<D> 变小的百分比,默认4

并发收集器Concurrent

用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾收集线程在执行的时候不会停顿用户程序的运行。适合对响应时间有要求的场景。本文只是简单介绍,之后会单独出对应文章

开启参数

  • CMS

    1
    -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
  • G1

    1
    -XX:+UseG1GC

CMS (Concurrent Mark-Sweep)收集器

核心是 标记-清除 ,优点是停顿时间最短,缺点是内存碎片,适用于Web

  • 过程
    • 初始标记(STW) 仅标记 GC Root 直接引用的对象
    • 并发标记 从 GC Root 出发,标记可达对象
    • 重新标记 标记 并发标记 过程中,变更的对象
    • 并发清除 清除无用对象
  • 降级 Concurrent Mode Failure
    • 并发标记、清理过程、work thread 在运行,申请老年代可能失败
    • 失败后会 降级 (临时启动 Serial Old 收集器)

缺点:

  1. CPU敏感 例如在微服务体系中的某个小服务,配置是2h,启动了一个垃圾回收线程,那么就只有一个 CPU 可以响应用户的请求
  2. 浮动垃圾 用户线程和垃圾回收线程同时工作,在GC时候,用户线程还是在做内存分配的
  3. 空间碎片
  • 相关参数

    1
    2
    3
    4
    5
    6
    7
    -XX:ConcGCThreads # 并发的GC线程数 default: 0(jdk8)
    -XX:UseCMSCompactAtFullCollection # FullGC之后做压缩 default: true
    -XX:CMSFullGCsBeforeCompaction # 多少次FullGC之后压缩一次 default: 0
    -XX:CMSInitiatingOccupancyFraction # Old区占用百分比后触发FullGC 该参数必须配合UseCMSInitiatingOccupancyOnly使用才有效 default: -1,负值表示使用CMSTriggerRatio->default: 80
    -XX:+UseCMSInitiatingOccupancyOnly # 是否开启CMSInitiatingOccupancyFraction占用率 default: false
    -XX:+CMSScavengeBeforeRemark # FullGC之前先做YGC 减少gc roots扫描的对象数,从而提高remark的效率 default: false
    -XX:+CMSClassUnloadingEnabled # 是否开启类卸载 如果开启 在full gc是会顺带扫描回收metaSpace/PermGen default: true(jdk8)

G1 收集器

Region G1中的块的概念,

SATB Snapshot At The Beginning 通过Root Tracing 得到的,GC开始时候存活对象的快照

RSet 记录了其他 Region 中的对象引用本 Region 中对象的关系,属于 points-into 结构(谁引用了我的对象)

  • Young GC

    与其他年轻代收集方式差不多,新对象进入 Eden 区,存活对象拷贝到S区,存活时间达到年龄阈值时,对象晋升到Old区

  • Mixed GC

    不是 Full GC,回收所有的 Young 和部分 Old

    Global Concurrent Marking

    1. Initial Marking Phase(STW): 标记 GC Root
    2. Root region Scanning Phase: 标记存活 Region
    3. Concurrent Marking Phase: 标记存活的对象
    4. Remark Phase(STW): 重新标记
    5. Cleanup Phase: 部分STW

    Mixed GC时机

    • InitiatingHeapOccupancyPercent 堆占有率达到这个数值则触发 Global Concurrent Marking ,默认 45
    • G1HeapWastePercentGlobal Concurrent Marking 结束之后,可以知道 Region 有多少空间要被回收,在每次YGC之后和再次发生 Mixed GC 之前,会检查垃圾占比是否达到此参数,达到了下次才会发生 Mixed GC ,默认 5(jdk8)
    • G1MixedGCLiveThresholdPercent Old 区的 region 被回收时候的存活对象占比需要达到多少,默认值 85
    • G1MixedGCCountTarget 一次 Global Concurrent Marking 之后,最多执行 Mixed GC 的次数,默认值 8
    • G1OldCSetRegionThresholdPercent 一次 Mixed GC 最多能回收Old区的 Region ,默认值 10
  • 常用参数

    1
    2
    3
    4
    5
    6
    7
    -XX:G1HeapRegionSize=n # region的大小,1-32M,2048个 default: 0
    -XX:MaxGCPauseMillis=200 # 最大停顿时间
    -XX:G1NewSizePercent # Young区的最小占比 default: 5
    -XX:G1MaxNewSizePercent # Young区的最大占比 default: 60
    -XX:G1ReservePercent # 保留空间防止to space溢出 default: 10
    -XX:ParallelGCThreads=n # SWT线程数 default: 2(jdk linux)
    -XX:ConcGCThreads=n # 并发线程数=1/4*并行 default: 0

G1中避免使用-Xmn、-XX:NewRatio等显式设置Young区的大小,会覆盖停顿时间目标

垃圾收集器搭配

img

实线的代表可以搭配使用的,例如 Old 区使用 CMS ,Young 区则可以使用 Serial、ParNew

虚线的代表 CMS 可以退化为 SerialOld(可以通过压缩减少碎片、内存使用率增长较快则降低触发FullGC 阈值来避免退化)

选择垃圾收集器

  • 优先调整堆的大小让服务器自己来选择
  • 如果内存小于100M,使用串行收集器
  • 如果是单核,并且没有停顿时间的要求,串行或者JVM自己选
  • 如果允许停顿时间超过1秒,选择并行或者JVM自己选
  • 如果响应时间最重要,并且不能超过1秒,使用并发收集器

可视化GC日志分析工具

日志打印相关参数

1
2
3
4
5
6
-XX:+PrintGCDetails # 打印GC日志详情
-XX:+PrintGCTimeStamps # 打印时间戳
-XX:+PrintGCDateStamps # 打印日期戳
-Xloggc:${PRO_NAME}/logs/gc.log # GC日志文件路径
-XX:+PrintHeapAtGC # 在每次GC的前后打印堆的使用量
-XX:+PrintTenuringDistribution # 发生GC时候打印Young区 复制年龄信息

日志格式(CMS)

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
1. {Heap before GC invocations=40 (full 0):
1.1. par new generation total 120192K, used 116952K [0x00000000f0000000, 0x00000000f8000000, 0x00000000f8000000)
1.2. eden space 109312K, 100% used [0x00000000f0000000, 0x00000000f6ac0000, 0x00000000f6ac0000)
1.3. from space 10880K, 70% used [0x00000000f6ac0000, 0x00000000f7236058, 0x00000000f7560000)
1.4. to space 10880K, 0% used [0x00000000f7560000, 0x00000000f7560000, 0x00000000f8000000)
1.5. concurrent mark-sweep generation total 131072K, used 103011K [0x00000000f8000000, 0x0000000100000000, 0x0000000100000000)
1.6. Metaspace used 92140K, capacity 94694K, committed 95104K, reserved 1132544K
1.7. class space used 10753K, capacity 11309K, committed 11392K, reserved 1048576K
2. 2019-01-24T13:37:49.590+0800: 33.245: [GC (Allocation Failure) 2019-01-24T13:37:49.590+0800: 33.245: [ParNew
Desired survivor size 5570560 bytes, new threshold 1 (max 15)
- age 1: 7451168 bytes, 7451168 total
: 116952K->8689K(120192K), 0.0155957 secs] 219963K->115232K(251264K), 0.0156544 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
3. Heap after GC invocations=41 (full 0):
3.1. par new generation total 120192K, used 8689K [0x00000000f0000000, 0x00000000f8000000, 0x00000000f8000000)
3.2. eden space 109312K, 0% used [0x00000000f0000000, 0x00000000f0000000, 0x00000000f6ac0000)
3.3. from space 10880K, 79% used [0x00000000f7560000, 0x00000000f7ddc480, 0x00000000f8000000)
3.4. to space 10880K, 0% used [0x00000000f6ac0000, 0x00000000f6ac0000, 0x00000000f7560000)
3.5. concurrent mark-sweep generation total 131072K, used 106543K [0x00000000f8000000, 0x0000000100000000, 0x0000000100000000)
3.6. Metaspace used 92140K, capacity 94694K, committed 95104K, reserved 1132544K
3.7. class space used 10753K, capacity 11309K, committed 11392K, reserved 1048576K
}
4. 2019-01-24T13:37:49.606+0800: 33.261: [GC (CMS Initial Mark) [1 CMS-initial-mark: 106543K(131072K)] 117412K(251264K), 0.0029414 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
4.1. 2019-01-24T13:37:49.609+0800: 33.264: [CMS-concurrent-mark-start]
4.2. 2019-01-24T13:37:49.707+0800: 33.362: [CMS-concurrent-mark: 0.096/0.098 secs] [Times: user=0.19 sys=0.00, real=0.10 secs]
4.3. 2019-01-24T13:37:49.707+0800: 33.362: [CMS-concurrent-preclean-start]
4.4. 2019-01-24T13:37:49.710+0800: 33.365: [CMS-concurrent-preclean: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
4.5. 2019-01-24T13:37:49.710+0800: 33.365: [CMS-concurrent-abortable-preclean-start]
# ... 省略 before after gc detail
4.6. 2019-01-24T13:37:50.004+0800: 33.659: [GC (Allocation Failure) 2019-01-24T13:37:50.004+0800: 33.659: [ParNew
Desired survivor size 5570560 bytes, new threshold 1 (max 15)
- age 1: 5574000 bytes, 5574000 total
: 118001K->6415K(120192K), 0.0127596 secs] 224544K->114652K(251264K), 0.0128342 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
4.7. 2019-01-24T13:37:50.336+0800: 33.991: [CMS-concurrent-abortable-preclean: 0.411/0.626 secs] [Times: user=1.02 sys=0.09, real=0.63 secs]
4.8. 2019-01-24T13:37:50.336+0800: 33.991: [GC (CMS Final Remark) [YG occupancy: 105816 K (120192 K)]2019-01-24T13:37:50.336+0800: 33.991: [Rescan (parallel) , 0.0214420 secs]2019-01-24T13:37:50.357+0800: 34.013: [weak refs processing, 0.0013584 secs]2019-01-24T13:37:50.359+0800: 34.014: [class unloading, 0.0203511 secs]2019-01-24T13:37:50.379+0800: 34.035: [scrub symbol table, 0.0189360 secs]2019-01-24T13:37:50.398+0800: 34.054: [scrub string table, 0.0010303 secs][1 CMS-remark: 108237K(131072K)] 214053K(251264K), 0.0639900 secs] [Times: user=0.08 sys=0.00, real=0.06 secs]
4.9. 2019-01-24T13:37:50.400+0800: 34.055: [CMS-concurrent-sweep-start]
# ...
5.0. 2019-01-24T13:37:50.443+0800: 34.098: [CMS-concurrent-reset-start]

日志详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 中括号的内容是内存地址
# 1. GC前堆的占用情况
# 1.1. 年轻代总大小 120192K,已用 116952K (f8000000-f0000000 = 134217728/1024 = 131072-total(120192)=10880),即年轻代中的s区实际上是有一个大小为10880的空间是浪费的
# 1.2. eden区使用情况,使用率100%,大小 109312K(f6ac0000-f0000000=111935488/1024=109312)
# 1.3. s-from区使用情况,使用率70%,大小 10880K,(f7236058-f6ac0000=7823448/1024 = 7640) /10880=70%
# 1.4. s-to区使用情况,使用率0%,大小 10880K
# 1.5. 老年代使用情况,大小 131072K,使用 103011K,占比78.6%
# 1.6. 元数据空间使用情况(持久代小 94694K,使用 92140K
# 1.7. 元数据空间中类占用的空间情况,大小 11309K,使用 10753K
# 2. 新生代回收,从109312K减少至10880K,新分配了20192k 整个内存空间从109312K减少至19172K,总内存空间分配了251264K,耗时0.032s左右,年龄是1;[Times: user=0.05 sys=0.02, real=0.03 secs] 可以看到user+sys(进程实际消耗的CPU时间)>real(调用从开始到结束的实际持续时间),说明我们是多个cpu执行
# 3. GC后堆的占用情况 invocations=41 指的是已经进行41次gc
# 3.1. 年轻代总大小 120192K,已用 8689K
# 3.2. eden区使用情况,使用率0%,大小 109312K
# 3.3. s-from区使用情况,使用率79%,大小 10880K,(f7ddc480-f7560000=8897664/1024 = 8689) /10880=79.8%
# 3.4. s-to区使用情况,使用率0%,大小 10880K
# 3.5. 老年代使用情况,大小 131072K,使用 106543K,占比81.3%
# 4. 由-XX:CMSInitiatingOccupancyFraction参数默认80%可知,old区占比已经大于阈值,将要进行full-gc,此阶段是初始标记阶段,只是标记一下GC Roots能直接关联的对象,是STW的
# 4.1&4.2. 并发标记阶段,和用户线程并发执行,主要作用是标记可达的对象
# 4.3&4.4. 预清理阶段
# 4.5&4.7. 此阶段的目的是使cms gc更加可控一些,作用也是执行一些预清理,以减少Rescan阶段造成应用暂停的时间
# 4.6. 先执行一次ParNew GC
# 4.8. 重新扫描标记阶段因用户线程继续运作而导致的变动对象,是STW的
# 4.9. 并发清理
# 5.0. 重置线程参数

工具