JVM 系列——垃圾回收

  本篇博客基于 Java8 与 HotSpot VM 论述,将从垃圾回收算法、收集器、参数调优、实战等方面帮助你理解 JVM 的垃圾回收机制,如果文内有内容不对的地方还请多多指教。

JVM 垃圾回收概述

在了解 JVM 垃圾回收之前我们需要知道以下几部分知识,分别对应了章节讲解:

  1. JVM 堆内存模型是怎样的?
  2. JVM 如何判断对象存活?有哪些垃圾回收算法?
  3. JVM GC 的触发条件什么?
  4. Java 程序应该如何设置 JVM 的启动参数?有哪些常用参数?

在充分了解以上问题的前提下,我们可以通过一些简单的例子实战 JVM GC 来加深印象。

JVM 堆内存模型

JVM 堆内存用来存放由 new 建出来的对象和数组,分为新生代与老年代两个区域,我们通过下方这张图来快速了解一下:

新生代

新生代默认占据着 1/3 的堆内存空间,包括 Eden、From、To 三个区域,其中 From、To 两个区域统称为 Survivor 幸存者区:

  1. Eden:大多数情况下,创建对象的内存会在 Eden 中进行分配,当 Eden 区内存满将进行 Minor GC;
  2. Survivor:经过 Minor GC 后存活的对象(即幸存者)将会进入 Survivor 区域,Survivor 区域的存在是为了防止幸存者全部进入老年代使其很快被填满,从而触发 Full GC;

为什么 Survivor 区域要分为 Form 与 To 两个子区域?

我将结合知乎的问答 JVM中新生代为什么要有两个Survivor(form,to)? 来谈谈自己的理解,建议先查看 垃圾回收算法 这一章节,有助于更好的理解问题。

原因是新生代 gc 比较频繁、对象存活率低,用复制算法在回收时的效率会更高,而复制算法需要两块内存,每次使用一块置空一块。
那有同学就会想,我 Eden + 一个 Survivor 两个区域也能实现复制算法呀。当然,但这样做的代价就是要将内存折半,为了不闲置过多的内存,新生代引入了改进复制算法,而该算法所需的内存结构就是如此。

老年代

老年代默认占据着 2/3 的堆内存空间,此区域的对象只有在 Full GC 的时候才会被清理,清理时应用会进入 STW 状态,需要清理的内存越大,STW 的时间就越长。

STW(stop the world) 是指:等待所有用户线程进入安全点后并阻塞,做一些全局性操作的行为。

JVM 垃圾回收算法

JVM 垃圾回收是指将死亡对象占用的内存回收,腾出空间给新对象使用,所以垃圾回收必然要面对的两个问题是:怎么判断对象死亡?如何高效回收死亡对象的内存?

垃圾回收算法可以参考这篇文章 JVM垃圾回收算法 - 简书,我站在巨人的肩膀上总结点内容。

如何判断对象存活

  1. 引用计数法:给每个对象设置一个计数器记录被引用的次数,只要次数大于 0 代表对象还在被使用,此方法无法解决循环引用,JDK1.1 之后不再使用。

  1. 可达性分析法:通过一些 GC Roots 对象为起点,构成一条引用链,当对象无法链接到 GC Root 时视为不可用。

哪些对象可以作为 GC Root?

在 java 中,可作为 GC Root 的对象有:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中的类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI(即一般说的 Native 方法)中引用的对象;

垃圾回收算法

本章节对应前面提出的第二个问题,如何高效回收死亡对象的内存?垃圾回收算法即回收死亡对象内存的策略,下方介绍了四种垃圾回收算法。

  1. 标记清除算法:标记存活对象,直接清除未被标记对象的内存空间,会产生内存碎片。

产生内存碎片有什么坏处呢?

通过 malloc 申请内存空间时,操作系统会从空闲内存链表中查找需要的内存空间,找到匹配的空间后将其标为已用,并从空闲链表中剥离,提供给应用程序使用。当有太多的内存碎片时,空闲链表中存在很多间隔的小空间,将导致较大的内存空间无法申请,从而降低了内存空间的利用率

  1. 标记整理算法:在标记清除算法之后,将存活对象的内存整理成连续的,解决内存碎片问题。

  1. 复制算法:将内存均分为 A、B 两块,用完 A 就将活的对象复制到 B 并清理 A,如此循环反复。

  1. 分代收集算法:对象优先在 Eden 区域分配,如果对象过大或者长时间存活则直接分配到 Old 区域。值得注意的是,新生代(Eden + Survivor)部分使用的是改进复制算法作为垃圾回收策略

改进复制算法有什么优势?

该算法相对传统复制算法对比,在大部分场景下可以让内存被“浪费”的更少。

IBM公司的专门研究表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。

基于上述的理论,HotSpot 虚拟机默认设定的 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然。98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

JVM GC 触发条件

在知道如何判断与回收“垃圾”后,紧接着就是在什么时机触发垃圾回收,本章节基于 JVM 的分代收集算法讲述其垃圾回收的时机问题。

Minor GC

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,它的触发条件很简单,当 Eden 区满时就会触发一次 Minor GC。

Full GC

清理整个堆空间(包括年轻代和永久代)被称为 Full GC,其触发条件如下:

  1. System.gc()方法的调用;
  2. 老年代空间不足;
  3. 方法区空间不足;
  4. 由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小;
  5. 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存,这个条件可能不是很好理解,正确的理解它需要先搞懂空间分配担保规则;

什么是空间分配担保规则?

在执行任何一次Minor GC之前,JVM会检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小。为啥会检查这个呢?因为最极端的情况下,可能新生代的 Minor GC 过后,所有对象都存活下来了,那岂不是新生代所有对象全部都要进入老年代?

  1. 如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次Minor GC了,也可以转移到老年代去。
  2. 如果执行 Minor GC 之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,就有可能出现老年代内存空间不足以转移新生代存活对象的情况。

所以在 Minor GC 之前会检查老年代的内存大小,是否大于之前每一次 Minor GC 后进入老年代的对象的平均大小,如果平均大小大于老年代的可用内存就会进行一次 Full GC。

这个担保判断还跟参数 -XX:+HandlePromotionFailure 有关,该参数开启后才会进行空间分配担保,java5 以前是默认不启用,java6 以后默认启用。

JVM 垃圾收集器与相关参数

读到这里相信你已经基本明白了 JVM 的垃圾回收机制,本章节讲解**如何给 JVM 设置参数?**因为有一些参数是结合特定的垃圾收集器才会起效果,所以顺带提几种垃圾收集器。

本章节是为 JVM GC 实战 做知识储备,在实战章节我们将通过调节 JVM 的参数观测应用 GC 的变化,以此来加深学习印象。

参数设置规则

JVM 的参数设置包括两种,不需要使用 -XX 开头的与需要的,前者主要包括 -Xms、-Xmx、-Xmn、-Xss 这四个,后者 -XX 开头参数被称为不稳定参数,其语法规则包含以下内容:

  1. 布尔类型参数值:如 -XX:+PrintGCDetails
  • + 表示启用该选项
  • - 表示关闭该选项
  1. 数字/字符串类型参数值:如 -XX:SurvivorRatio=8
  • = 使用等号赋值

常用参数分类

  1. 内存相关
  • -Xmx20M:设置老年代的容量最大为 20M,默认 1/4 物理内存;
  • -Xms20M:设置老年代的容量最小为 20M,默认 1/64 物理内存;
  • -Xmn6M:设置新生代的大小为 6M,此值等同于设置相同的 -XX:NewSize 和 -XX:MaxNewSize;
  • -Xss128k:设置每个线程的堆栈大小为 128K;
  • -XX:NewRatio=2:设置老年代内存是年轻代的 2 倍,即年轻代占 1/3 的内存,一般情况下,该值不允许小于 1;
  • -XX:NewSize:设置新生代的初始内存大小;
  • -XX:MaxNewSize:设置新生代的最大内存大小;
  • -XX:SurvivorRatio=8:Eden 区占新生代的 8/10,Survivor 占 2/10,稍大的 Survivor 空间可以提高在年轻代回收生命周期较短的对象的可能性,如果 Survivor 不够大,一些短命的对象可能直接进入年老代,这对系统来说是不利的;
  1. 辅助调优相关
  • -XX:+PrintGC:打印 GC 简要日志;
  • -XX:+PrintGCDetails:打印 GC 详细日志;
  • -XX:+PrintHeapAtGC:每次 GC 过后都打印堆内存占用信息;
  • -XX:+PrintGCDateStamps:打印 GC 发生的时间;
  • -XX:+HeapDumpOnOutOfMemoryError:当抛出 OOM 时进行 HeapDump;
  • -XX:+HeapDumpPath:指定 HeapDump 的文件输出目录;
  • -XX:+DisableExplicitGC:使显式调用 System.gc() 失效;
  • -XX:+PrintTenuringDistribution:打印 Survivor 区的对象年龄;
  • -XX:+PrintVMOptions:打印虚拟机接受到的命令行显式参数;
  • -XX:+PrintCommandLineFlags:打印传递给虚拟机的显式和隐式参数;
  • -XX:+PrintFlagsFinal:打印所有系统参数;
  1. 串行收集器相关

Serial 串行收集器有两个特点:第一,它仅仅使用单线程进行垃圾回收;第二,它独占式的垃圾回收。其在进行垃圾回收时,Java 应用程序中的线程都需要暂停的。

Serial 有新生代与老年代两个版本,前者使用复制算法,后者使用标记整理算法。

  • -XX:+UseSerialGC:在新生代和老年代使用串行回收器;
  • -XX:PretenureSizeThreshold=1000000:将 1MB 以上的对象直接在年老代分配;
  • -XX:MaxTenuringThreshold=15:超过 15 岁的对象移入老年代;
  1. 并行收集器相关

ParNew 并行收集器是只工作在新生代的垃圾收集器,它只简单地将串行回收器多线程化。它的回收策略、算法以及参数和串行回收器一样。

并行回收器也是独占式的回收器,在收集过程中,应用程序会全部暂停。其性能与串行收集器相比,取决于服务器的 CPU 的能力,并行回收期能够更好的利用 CPU 的并行能力。

  • -XX:+UseParNewGC:在新生代使用并行收集器;
  • –XX:ParallelGCThreads=20:设置 20 个线程进行垃圾回收;
  1. 并行回收收集器相关

Parallel Scavenge 和并行收集器不同的地方是,它非常关注系统的吞吐量,jdk7、jdk8 默认使用 Parallel Scavenge 作为新生代收集器。
吞吐量 = 运行用户代代码时间 /(运行用户代码时间 + 垃圾收集时间)

Parallel Scavenge 有新生代与老年代两个版本,前者使用复制算法,后者使用标记整理算法。

  • -XX:+UseParallelGC:指定新生代使用并行回收收集器;
  • -XX:+UseParallelOldGC:指定老年代使用并行回收收集器;
  • -XX:MaxGCPauseMillis=200:指定收集器尽可能在 200ms 时间内完成内存回收;
  • -XX:GCTimeRatio=99:指定收集器的吞吐量为 99%;
  1. CMS 收集器相关

与并行回收收集器不同,CMS 收集器主要关注于系统停顿时间。CMS 是 Concurrent Mark Sweep 的缩写,意为并发标记清除,从名称上可以得知,它使用的是标记-清除算法,同时它又是一个使用多线程并发回收的垃圾收集器。

CMS 收集器是工作在老年代的垃圾回收器。

  • -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用 CMS + 串行收集器;
  • -XX:+ParallelCMSThreads:设定 CMS 的线程数量;
  • -XX:+UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩;
  • -XX:+CMSParallelRemarkEndable:启用并行重标记;
  • -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收;
  • -XX:CMSInitatingPermOccupancyFraction=68:当永久区占用率达到 68% 后,启动 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了);
  • -XX:+UseCMSInitatingOccupancyOnly:只在到达阈值的时候,才进行 CMS 回收;
  • -XX:CMSInitiatingOccupancyFraction=68:当老年代占用率达到 68% 后,启动 CMS 回收;
  1. G1 收集器相关

G1 收集器的目标是作为一款服务器的垃圾收集器,因此,它在吞吐量和停顿控制上,预期要优于 CMS 收集器。

G1 收集器能够工作于新生代与老年代。

  • -XX:+UseG1GC:使用 G1 垃圾回收器;
  • -XX:+UnlockExperimentalVMOptions:允许使用实验性参数;
  • -XX:MaxGCPauseMills=20:设置垃圾收集最大允许的停顿时间为 20ms;
  • -XX:GCPauseIntervalMills=200:设置停顿间隔时间为 200ms;
  1. 其他
  • -XX:+HandlePromotionFailure:开启空间分配担保,java6 以后默认启用,该参数在 jdk6 uptate 24 后已经不起效果;
  • -XX:TargetSurvivorRatio=90:使 from 区使用到 90% 时,再将对象送入年老代;
  • -XX:MinHeapFreeRatio=40:设定堆空间最小空闲比例,当堆空间的空闲内存小于 40% 时,JVM 便会扩展堆空间;
  • -XX:MaxHeapFreeRatio=70:设定堆空间最大空闲比例,当堆空间的空闲内存大于 70% 时,便会压缩堆空间,得到一个较小的堆;

上面根据各个垃圾回收器做了一些参数分类,总的来说可以参考下图,有连线的代表可以搭配使用。

JVM GC 实战

本章节我们将通过一些示例代码结合 JVM 的部分参数,来观察 JVM GC 的情况,完成下面的实战内容有助于加深你对各个 JVM 参数的印象,了解 JVM 调优的过程。

实战一、Minor 与 Full GC 的触发

我们通过一个简单的示例来观测 JVM 在指定场景下的 GC 情况,示例代码清单 JVMMemoryDemo.java 点此访问,该示例可以根据用户输入的配置来模拟对象创建内存分配的过程,运行后会要求输入以下两个值:

  1. 内存分配的最小单元:每次分配内存的大小,将此值调大可以模拟大对象的创建;
  2. 本次分配的内存大小:本次测试总计分配的内存大小;

例如输入 1m、8m 代表应用总计分配 8M 内存,每次分配 1M,将分配 8 次,等价于创建了 8 个大小为 1M 的对象。

通过下列方式可获得本例的 jar 文件,点击此处可查看示例的源代码
Linux & Mac:wget https://resources.chenjianhui.site/jvm-gc-demo-1.1.jar
Windows:浏览器访问 https://resources.chenjianhui.site/jvm-gc-demo-1.1.jar 下载

在这个例子中我们采用 PrintGCDetails 配置将每次 GC 的日志打印了出来,观察日志可以提取如下信息:

我们对这 5 次 GC 的原因稍作分析。

  • 前 4 次 Minor GC:触发原因很简单,eden 区空间只有 2048K,而 12 份 512K 的对象在此处分配导致空间不足,触发了 Minor GC。
  • 最后 1 次 Full GC:最后一次 Minor GC 进行空间分配担保时,老年代剩余空间小于历次 Minor GC 进入老年代的平均大小(约为1500K),于是触发了一次 Full GC。

在知道原因后想要避免这 5 次 GC 并不难,下面假设应用只有 10M 的可用内存,提供几种优化的方案:

  1. 设置大于 512k 的对象直接进入老年代,这样所有对象直接分配在老年代,Minor GC 不被触发,Full GC 也不会因为空间分配担保规则被触发。此处年轻代被使用了 985K 是因为我们的程序除了用户输入的对象分配,还有如 Scanner、String 等对象也需要分配内存。
  1. 设置每次分配的对象为 1M,在分配第 2 个 1M 的对象时,年轻代空间不足,该对象被分配到老年代,后续的几个对象同理,同样不会触发 GC。

实战二、假设场景下的 JVM 调优

实战之前我们要明白,“调优”是在调什么?为什么要做JVM调优?

  1. 调优其实是在调整 JVM 的参数,使应用运行更好的适应指定场景。
  2. 调优的目标是解决以下几个问题:
  • 防止与解决 OOM:根据应用运行的数据情况合理的调整内存分配;
  • 降低 GC 消耗的时间,减少 Full GC 出现的频率:频繁的 GC 会导致系统卡顿;

接下来我们来假设一个电商秒杀的场景,具体描述如下:

  1. 一个电商网站,准备做一个秒杀活动;
  2. 电商网站目前的用户量为 100W,假设 30% 的用户参与秒杀,且秒杀持续时间为 5min,那么每秒产生的订单为 1000 个;
  3. 订单支付又涉及到发起支付流程、物流、优惠券、推荐、积分等环节,导致产生大量对象,我们假设整个过程产生的对象约等于 30K,那么每秒在 Eden 区生成的对象约等于 30M,整个过程需要消耗的内存约为 9000M;
  4. 如果一个订单的处理会在 3s 内完成,那么在产生 Minor GC 时不可回收的对象约占 90M;
  5. 假设我们的服务器现在只有 600M 内存应用于这个场景;

我们通过一段 Java 代码来模拟上述流程,示例代码清单 JVMGCDemo.java 点此访问,该示例允许用户输入两个值进行流程模拟:

  1. 平均流量:代表网站持续占用的内存量,在这里我们设置为 90M。
  2. 总流量:代表场景总计消耗的内存量,在这里我们设置为 9000M。

尽管此处使用“流量”这个名词不太合适,但是我暂时没有想到更好的描述词了。

示例的 jar 文件可通过 https://resources.chenjianhui.site/jvm-gc-demo-1.1.jar 链接获取

根据日志可以看出应用进行了 160 次 GC,一共消耗了 2267ms,可以看到堆的初始大小(Total_Memory)为 123MB,这是因为应用运行在 8G 的机器上,堆默认的初始大小为物理内存的 1/64 即 128M 左右。

上面我们计算过,应用的总内存占用为 9000M 是大于 128M 的,为了避免堆扩张操作并减少 GC 次数,我们可以通过 -Xms 配置出一个稳定的堆。

什么是稳定的堆?

是使 -Xms 和 -Xmx 的大小一致,即最大堆和最小堆 (初始堆) 一样,这样的堆便称之为稳定堆,一般来说,稳定的堆大小对垃圾回收是有利的。但是一个不稳定的堆并非毫无用处,稳定的堆大小虽然可以减少 GC 次数,但同时也增加了每次 GC 的时间。

调整过后可以明显看到 GC 次数少了,Full GC 次数和耗时降低很明显,而 Minor GC 的次数虽然降低了但整体耗变动不大,这是由于单次 Minor GC 的耗时是更高的。

不难看出应用 GC 的主要耗时是在年轻代的垃圾回收上,下面我们通过 NewRatio 配置调整一下年轻代的内存分配比例,再次降低 Minor GC 的次数。

第二次调整过后 GC 消耗的总时间降低了一倍,由于老年代空间减小了(NewRatio 默认为 2),导致 Full GC 多了两次,但是 Monor GC 次数减少了两倍多,整体性能提升明显。

总结

理解 JVM 的垃圾回收机制对生产应用的调优是非常有帮助的,因为 JVM 调优是一个理论与经验并重的场景,它没有通用范式来套用,需要根据生产应用的运行状况一点点来调节。

我们回到文章标题 JVM垃圾回收概述 提到的四个问题,现在你应该能够回答它们了。

  1. JVM 堆内存模型是怎样的?
  2. JVM 如何判断对象存活?有哪些垃圾回收算法?
  3. JVM Minor GC 与 Full GC 的触发条件什么?
  4. Java 程序应该如何设置 JVM 的启动参数?有哪些常用参数?

参考资料

附录

JVMMemoryDemo 代码清单

import java.util.Scanner;

/**
 * @author JianhuiChen
 * @description JVM 内存分配的示例
 * @date 2020-06-30
 * @version 1.1
 */
public class JVMMemoryDemo {

    public static void main(String[] args) {
        JVMUtils.printJVMInfo();

        Scanner scan = new Scanner(System.in);
        System.out.println("请输入内存分配的最小单元,支持 b、k、m、g 作为单位");
        final long singleSize = JVMUtils.parseBitSize(scan.next());
        System.out.println("请输入需要分配的内存大小,支持 b、k、m、g 作为单位");
        final long bitSize = JVMUtils.parseBitSize(scan.next());

        // 按照最小单元切割,分多次分配堆空间,考察堆空间的使用情况
        final int allotCnt = (int) Math.ceil(bitSize / (double) singleSize);
        System.out.println(String.format("将进行 %d 次内存分配", allotCnt));

        byte[][] mermory = new byte[allotCnt][];
        for (int i = 0; i < allotCnt - 1; i++) {
            mermory[i] = new byte[(int) singleSize];
        }
        final int lastBitSize = (int) (bitSize - singleSize * (allotCnt - 1));
        mermory[allotCnt - 1] = new byte[lastBitSize];

        JVMUtils.printJVMGCInfo();
    }
}

JVMGCDemo 代码清单

import java.util.Scanner;

/**
 * @author JianhuiChen
 * @description JVM 垃圾回收示例
 * @date 2020-07-06
 * @version 1.1
 */
public class JVMGCDemo {
    public static void main(String[] args) {
        JVMUtils.printJVMInfo();

        // 单个对象大小 100k
        final long singleSize = JVMUtils.parseBitSize("100k");

        Scanner scan = new Scanner(System.in);
        System.out.println("请输入平均流量,支持 b、k、m、g 作为单位");
        final long occupySize = JVMUtils.parseBitSize(scan.next());
        System.out.println("请输入总流量,支持 b、k、m、g 作为单位");
        final long bitSize = JVMUtils.parseBitSize(scan.next());

        long startAt = System.currentTimeMillis();

        final int allotCnt = (int) Math.ceil(occupySize / (double) singleSize);
        byte[][] mermory = new byte[allotCnt][];
        final int total = (int) Math.ceil(bitSize / (double) singleSize);
        for (int i = 0; i < total; i++) {
            mermory[i % allotCnt] = new byte[(int) singleSize];
        }

        JVMUtils.printJVMGCInfo();

        System.out.println(String.format("运行耗时 %dms", System.currentTimeMillis() - startAt));
    }
}

JVMUtils 代码清单

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * @author JianhuiChen
 * @description 示例使用到的工具函数
 * @date 2020-06-30
 * @version 1.1
 */
public class JVMUtils {

    /**
     * 新生代 GC 日志带有的标志
     */
    private static final Set<String> yGCAlgorithm = new LinkedHashSet<String>() {
        {
            add("Copy");
            add("ParNew");
            add("PS Scavenge");
            add("G1 Young Generation");
        }
    };

    /**
     * 老年代 GC 日志带有的标志
     */
    private static final Set<String> oGCAlgorithm = new LinkedHashSet<String>() {
        {
            add("MarkSweepCompact");
            add("PS MarkSweep");
            add("ConcurrentMarkSweep");
            add("G1 Old Generation");
        }
    };

    /**
     * 输出 JVM 的配置
     */
    public static void printJVMInfo() {
        // 返回启动参数信息
        List<String> inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
        // 返回java虚拟机中的内存总量
        long totalMemory = Runtime.getRuntime().totalMemory();
        // 返回java虚拟机试图使用的最大内存量
        long maxMemory = Runtime.getRuntime().maxMemory();
        System.out.println(String.format("VM_Options = %s", inputArguments));
        System.out.println(String.format("Total_Memory(-Xms ) =  %dMB", totalMemory / 1024 / 1024));
        System.out.println(String.format("Max_Memory(-Xmx ) =  %dMB", maxMemory / 1024 / 1024));
    }

    /**
     * 输出 GC 信息
     */
    public static void printJVMGCInfo() {
        long gcCount = 0;
        long gcTime = 0;
        long oldGCount = 0;
        long oldGcTime = 0;
        long youngGcCount = 0;
        long youngGcTime = 0;
        for (final GarbageCollectorMXBean garbageCollector :
                ManagementFactory.getGarbageCollectorMXBeans()) {
            gcTime += garbageCollector.getCollectionTime();
            gcCount += garbageCollector.getCollectionCount();
            String gcAlgorithm = garbageCollector.getName();
            if (yGCAlgorithm.contains(gcAlgorithm)) {
                youngGcTime += garbageCollector.getCollectionTime();
                youngGcCount += garbageCollector.getCollectionCount();
            } else if (oGCAlgorithm.contains(gcAlgorithm)) {
                oldGcTime += garbageCollector.getCollectionTime();
                oldGCount += garbageCollector.getCollectionCount();
            }
        }
        System.out.println(String.format("GC: Cnt %d, Cost %dms", gcCount, gcTime));
        System.out.println(String.format("YGC: Cnt %d, Cost %dms", youngGcCount, youngGcTime));
        System.out.println(String.format("OGC: Cnt %d, Cost %dms", oldGCount, oldGcTime));
    }

    /**
     * 解析带单位的容量为 bit 大小
     * 1k => 1024 * 1024
     *
     * @param sizeStr 容量
     * @return bit 数值
     */
    public static long parseBitSize(String sizeStr) {
        if ("".equals(sizeStr) || sizeStr == null) {
            return 1024 * 1024;
        }
        sizeStr = sizeStr.toLowerCase();
        String type = sizeStr.substring(sizeStr.length() - 1);
        long sizeNum = Integer.parseInt(sizeStr.substring(0, sizeStr.length() - 1));
        long bitSize;
        switch (type) {
            case "k":
                bitSize = sizeNum * 1024;
                break;
            case "m":
                bitSize = sizeNum * 1024 * 1024;
                break;
            case "g":
                bitSize = sizeNum * 1024 * 1024 * 1024;
                break;
            default:
                bitSize = sizeNum;
        }
        return bitSize;
    }
}