JVM运行原理及优化之 jstat -gc

  • 我们写好的代码,是要通过JVM才能运行的
  • JVM 想要执行一个类,首先要加载类,在加载类之前,需要先编译成字节码class 文件
  • 然后就执行类的加载过程,JVM 加载类的话,需要类加载器
  • 类加载器是分层级的,遵循双亲委派机制,
    • 最上层是Bootstrap ClassLoder,加载java的核心类库,加载java安装目录下的lib目录的class文件
    • 第二层是Ext ClassLoder,加载一些java的其他类库,加载java安装目录下的lib/ext目录下的class
    • 第三层是Application ClassLoder ,应该程序类加载器,这个类加载器是加载我们写的类
    • 如果我们自定义类加载器的话,那就是第四层
    • 类加载器遵循双亲委派机制,就是说,如果要加载一个类,先去为他的父类能不能加载,如果父类上面还有父类,就继续问,直到顶层。然后顶层说加载不了,就下派到子类,如果所有父类都加载不了,那就自己加载。这么做的好处是,不会重复加载一个类
  • 然后说一下类加载的过程,分这么几步,加载,验证,准备,解析,初始化。 
    • 加载的话,就是刚才说的类加载器去加载类
    • 验证阶段,主要是验证加载的字节码是否符合JVM规范,不然随便瞎写JVM也执行不了
    • 准备阶段,主要是给对象申请内存,然后给变量设置初始值,该设置0的设置0,该设置null的设置null
    • 解析阶段,主要是给符号引用变成直接引用,就是把一些变量什么temp,直接换成物理地址,不然执行的时候JVM也不认识temp是啥
    • 初始化阶段:主要是给变量赋值,准备阶段只是设置了初始值,这个是核心阶段,执行类的初始化,如果发现这个类的父类没有初始化,会先暂停,然后去初始化父类,也是走类加载的一套流程,直到父类加载完了,再执行子类的初始化
  • 这是类加载的过程,加载的类是放到了JVM的元数据空间,也就是永久代。

 

 

 

  • 永久代的话,我们JVM参数一般会给设置个256M,这个一定要设置,绝对不能不设置JVM参数,使用默认JVM参数可能就给新生代分配一两百兆,永久代分配的可能也很少,一旦并发量上来,系统扛不住,永久代一般就放点类和常量池,一般给256M够了,如果给小了,可能导致频繁的Full GC,因为永久代如果满了,会触发Full GC,这个是很坑的
  • 类加载到永久代后,会把类交给字节码执行引擎去执行,画图
  • 执行这个操作是线程去执行的,每个线程都配有一个程序计数器和Java虚拟机栈

 

  • 因为Java是支持多线程的,所以必须要有程序计数器,记录这个线程执行到哪了
  • Java虚拟机栈在执行每个方法的时候,都会创建一个栈帧,main方法也一样
  • 局部变量都放到这个栈帧中,如果这个方法执行完了,局部变量也就失效了
  • 这里的栈帧如果没有执行完时,其实都是GC Root,垃圾回收时,就是根据这里的局部变量的引用和永久代的引用来判断对象是否存活
  • 我们设置JVM参数的时候,一般都会给Java虚拟机栈 1M的大小,一个系统运行最多几百个线程,不用设置太大,浪费内存,也不能设置太小,容易溢出,特别是递归调用的时候
  • 然后局部变量保存的都是对象的地址,地址指向了JVM堆内存

 

  • 如果是使用ParNew + CMS 垃圾回收器的话,堆内存分年轻代和老年代是很明确的,不像G1
  • 然后说一下ParNew垃圾回收器。
    • 这个垃圾回收器是回收年轻代的,使用的是多线程回收,不像之前的Serial回收器使用单线程回收。
    • 然后ParNew使用的是复制清除算法,把年轻代分为Eden区 和两个Survivor,JVM 参数默认的占比是 8:1:1,系统运行会把对象创建到Eden区,每次YoungGC 会标记存活对象,复制到Survivor0中,再次YoungGC时,再把存活对象复制到Survivor1中。系统运行期间会保证一直有一个Survivor是空着的。
    • Eden区的占比有时是可以调优的,如果条件有限,没有大内存的机器,然后对象创建的还特别频繁,存活的对象比较多,那就建议把Eden区比例调低一些,让Survivor大一点,宁可Young GC多一些,也不要让Survivor触发了动态年龄审核或者放不下存活对象。如果放不下那就把这批对象扔到老年代了,Full GC是很慢的。如果是调低Eden,YoungGC会很频繁,但是YoungGC特别快,我通过jstat 看,回收100M垃圾大概也就1ms,所以,如果内存实在不够,降低Eden去比例也不是不可以。但是如果有条件的话最好的话还是加大新生代内存,毕竟YoungGC也是要Stop the World的。
  • 然后继续说ParNew ,它非常适合回收年轻代内存。因为年轻代一般存活的对象是很少的,大多数都是刚创建出1毫秒就变成了垃圾,所以把极少数存活的对象标记出来,复制成本还是很低的,如果像老年代那样采用标记清除算法,那就太慢了

 

  • 然后说一下老年代的垃圾回收器CMS,这个垃圾回收器是使用的 标记-清除 + 整理 算法,我们一般在JVM参数会指定这算法和整理的频率,JVM参数默认是,标记-清除,5次之后,才会去整理内存空间,让对象整齐排列。但是这个默认参数不太好,这样做会有大量的内存碎片,如果某一次从年轻代晋升一个大对象,老年代居然找不到一块连续的内存,就会触发Full GC,那就坑了。我们会把那个值调成0,就是每次CMS垃圾回收后,都会整理内存,虽然每次的回收时间会多一些,但是不会出现内存碎片。

 

  • CMS 垃圾回收分为4个步骤
    • 第一步是初始标记:初始标记的话,只标记GC root直接引用的对象,只有很少一部分,这个阶段需要STW,但是影响不大,这个过程特别快。这个过程也可以优化,JVM 有个参数是初始标记阶段多线程标记,减少STW时间,正常是单线程标记的。
    • 第二步是并发标记:这个阶段是不需要STW的,是和系统并行的处理,系统继续运行,然后垃圾回收线程去追踪第一步标记的GC root,这一步是很耗时的,但是不影响程序执行。因为在垃圾回收时是允许系统继续创建对象的,所以这个过程会有新的对象进来,也会有标记存活的但是现在变成垃圾,这些有改动的对象JVM都会记下来,等待下一步处理。这一步有一个缺点,并发清理时也有这个问题,就是会占用CPU资源。如果是一个4核的机器,那会占用一个CPU去垃圾回收,公式是(cpu核数 + 3)/4。所以一般CPU资源负载特别高的时候,就俩情况,要不是程序的线程太多了。要不就是频繁FullGC,导致的。
    • 第三步是重新标记:重新标记阶段,会把并发标记阶段有改动的对象重新标记,这一步需要STW,不过也是比较快的,因为改动的对象不会特别多,但是要比第一步慢因为要重新判断找个对象是否GC可达。这里也可以通过JVM参数优化,可以通过参数控制,让CMS在重新标记阶段之前尽量触发一次Young GC(尽量YoungGC是因为可能新生代可能刚刚YoungGC不久,那此时就没必要再一次YoungGC了)这样做的好处是,改动的对象中从存活变为垃圾的那部分,就被清理掉了,缩短STW时间。虽然YoungGC也会造成停顿,但是YoungGC一般频率是比较快的,早晚都要执行,现在执行一举两得。
    • 第四步是并发清理,并发清理是和系统并行的,不需要STW。这个阶段是清理前几个阶段标记好的垃圾。
    • 最后,我们通过JVM参数设置,每次Old GC后都重新整理内存,整理阶段会把老年代零零散散的对象排列到一起,减少内存碎片。
  • 在说ParNew + CMS调优之前,我们先说下JVM的几种GC,Young GC,Old GC,Full GC
    • Young GC 和 Old GC 我上面都已经说过了
    • 再说下 Full GC 吧,Full GC就是全面回收整个堆内存,包括新生代、老年代、永久带。整个过程极其的慢,一定要减少Full GC的次数,一般出现频繁的Full GC有几种情况,我们要避免出现这几种情况
      • 第一种是,内存分配不合理,导致Survivor放不下,或者触发了动态年龄审核机制,频繁的往老年代放对象。
      • 第二种,有内存泄漏问题,导致老年代大部分空间被占用,回收都回收不掉,导致每次新生代晋升一点点对象,就放不下了,触发Full GC
      • 第三种,大对象,一般是代码层面的问题,创建了太多的大对象,大对象是直接放入老年代的, 大对象过多会导致频繁触发Full GC
      • 第四种,永久代满了,触发Full GC,我们JVM参数设置256M基本够了,如果不出现代码层面的bug ,一般不会出现这种情况
      • 第五种,有人在代码里误调用了System.gc(),写了这个方法后,如果有机会,JVM就会发生一次Full GC。不过JVM参数可以禁止这种情况,不允许主动调用,我们要加上
  • 一般什么情况下我们要警觉是不是频繁的Full GC了
    • 第一种情况,CPU负载折线上升,特别高
    • 第二种情况,系统卡死了,或者系统处理请求极慢
    • 第三种情况,如果公司有监控系统,会报警。。
  • 然后再说一下ParNew + CMS 调优的问题吧。如果一个系统需要JVM调优,那其实说白了就是Stop the World 太久了,导致系统太卡了。我们说的调优,其实就是减少STW的时间,让系统没有明显的卡顿现象。
  • 然后分析下,需要STW的有几个地方。YoungGC,和Old GC的两个阶段。但是YoungGC一般STW时间特别短,Old GC时间一般会是Young GC的几倍到几十倍,而且占用CPU资源严重。所以,我们优化的重点是让系统减少Old GC的次数。最好让系统只有YoungGC,没有Old GC,更没有Full GC
  • 所以,优化的重点就是尽量不要让对象进入老年代。如果对象进不去老年代,想Full GC都难。这是JVM调优的重点,对象进入老年代的情况也有几种
    • 第一种,对象经过15次YoungGC,依然是存活的,那晋升老年代
      • 这个其实我们是可以优化一下的,因为如果系统1分钟或者30秒一次YoungGC,那没必要非得让对象存活十几分钟才进入老年代,一般存活个两三分钟,这个对象大概率就是要存活很久的了。所以,我们当时是调低了这个参数的,设置了5。不然这个对象一直存活,然后在两个Survivor里来回复制,如果这个对象小一点还好,如果这个对象挺大的,那容易触发Survivor的动态年龄审核机制,让一大批对象进入老年代。所以,该进入老年代的对象,就让他赶紧进去。
    • 第二种,Young GC后存活的对象大小超过Survivor 的50%,那就会触发动态年龄审核机制,如:1岁、2岁、3岁、4岁的对象加起来大于Survivor 的50%,那大于等于4岁的对象全部进入老年代。
    • 第三种,Young GC后存活的对象大于Survivor的大小,那这一批对象直接全部进入老年代,特别坑。
    • 第四种,大对象直接进入老年代,这个JVM参数里是可以设置的,一般我们都设置1M,大于1M的对象进入老年代,一般很少有1M的对象,一般都是个大数组,或者map。
  • 第一种情况和第四种情况,一般是可控的。所以想要优化的话,主要是要在Survivor的大小这块下功夫。我们要避免动态年龄审核和Survivor放不下的情况。要想保证这点,我们就要知道,我们系统的高峰时期,JVM中每秒有多少对象新增,每次YoungGC存活了多少对象。这就需要用 jstat 了。
  • 首先要使用 jstat -gc PID 1000 1000
    • 找到JVM的PID,然后每秒打印一次JVM的内存情况,如果系统访问量比较小,每秒的增长不是很明显,那就把每次的间隔时间调大一点,比如一分钟打印一次
    • 通过这行命令,我们可以看到当时的内存使用情况,有几个列比较重要的数据
    • S0C:Survivor0 的大小
    • S1C:Survivor1 的大小
    • S0U:Survivor0 使用了多少
    • S1U:Survivor1 使用了多少
    • EC:Eden 区的大小
    • EU:Eden 区使用了多少
    • OC:老年代的大小
    • OU:老年代使用了多少
    • MC:永久代的大小
    • MU:永久代使用了多少
    • YGC:YoungGC次数
    • YGCT:YoungGC的总耗时
    • FGC:Full GC次数
    • FGCT:Full GC的总耗时
  • 一般使用 jstat 优化,重点观察这几个指标
    • Eden 区对象的增长速度
      • 上面的几列,通过一行数据是看不出来Eden 区每秒增长多少数据的,所以我们才每秒打印一次,通过上一秒和下一秒EU的数据就可以推断出每秒增长了多少。这个数据进来多打印几行,取个平均值。
    • Young GC 频率
      • 我们我们知道系统启动时间,用YGC的大小除也能算,但是谁没事记得系统什么时候启动的。而且如果我想看高峰时期某一段时间的呢,就看不了了。看几十天的平均值也没什么意义。所以这个高峰时段YoungGC的频率是通过,Eden的大小,除以Eden区对象的增长速度来算的,Eden区对象增长速度,我们已经知道了。
    • Young GC 耗时
      • 这个YoungGC耗时,我们取平均值就行,用YGCT除以YCG,时间除以次数就是每次的耗时。如果说就像看高峰时段的,因为CPU等使用率比较高,可能会影响回收时间,也可以单独看几次的YoungGC,算出时间。
    • Young GC 后多少对象存活
      • 这个指标还是比较重要的,我们要确定每次存活的对象Survovir到底能不能放得下。我们要保证每次存活的对象要小于Survivor的50%,否则就会触发动态年龄审核机制。
    • 老年代对象增量速度
      • 老年代对象增长速度,决定了Old GC的频率。发生Old GC后,FGC那一列也会增长,FGC那一列其实是FullGC 和Old GC的总和。经过优化后的JVM,每次YoungGC不应该进入太多的对象,不进入或者每次进入几兆是比较好的。这个指标我们也要分多次观察,因为只看一次YoungGC晋升的大小是片面的。我们现在已经知道了YoungGC的频率,如果是3分钟一次,那我们就3分钟打印一次内存情况。jstat -gc PID 180000 100,取多次晋升大小的平均值就行。如果晋升的对象特别多,我们需要分析这些对象为什么会进入老年代,上面我说了有四种情况会晋升老年代,到底是哪种情况。是Survivor不够大,还是大对象太多了,或者有内存泄漏导致对象回收不掉,进入了老年代。这个还要具体分析一下的。如果是Survivor太小,我们很轻易就能看出来,如果每次Young GC后S区都是0,那说明存活的对象太多,S区放不下,都进入了老年代。如果S区不是0,有一部分,但是每次回收进入老年代都很多,就有可能是触发动态年龄审核,这个最好再通过GC日志看一下,通过JVM参数可以让系统打印每次GC的日志。如果出现内存泄漏,数据一般是这样的,发现每次FGC次数加1后,老年代并没有多少数据被回收掉,占用了很多。这就大概率是内存泄漏,导致老年代回收不掉。如果是大对象,数据会这样显示,发现及时没有Young GC,OU也会一直在涨,因为大对象是不用经过年轻代的直接进入老年代。如果内存泄漏和大对象的情况,我们可以用 jmap 打印一份内存快照,用MAT工具分析一下到底是什么对象特别大,通过分析出来的堆栈信息就可以定位到代码的位置。
    • Full GC 频率多高
      • 看这个频率和看YoungGC的频率是一样的,可以看高峰时期某几次的平均值。这个Full GC是很耗时的,Full GC的频率我们最好控制在一天1次或者几天一次的范围。特别是对时效性要求比较高的系统,一定要减少Full GC次数。
    • 一次Full GC 的耗时
      • 这个可以取平均值,也可以取某一段的。我们会发现这个Full GC的耗时是YoungGC的好多倍
  • ParNew + CMS 的原理和优化大概就是这么样的,下面我说一下现在比较流行的G1回收器
  • 有人说G1比ParNew +CMS好,可以全面取代,没必要用ParNew和CMS了,我觉得不是这样的。G1有G1的优点,但是也有缺点。我们选择垃圾回收器还是要根据系统的实际情况来看。但是ParNew+CMS的确有不足的地方,如果某些系统使用的机器是大内存,16G、32G,那每次GC都要等Eden区放满了才执行垃圾回收,一次回收好几G的垃圾,那太慢了,可能停顿时间几十上百毫秒,Full GC甚至要几秒。那就太坑了,不可以接收。这个时候就必须用G1了。
  • 那什么情况下用ParNew+CMS呢,它又什么优点
    • 它的优点就是我们可以优化到极致,极致到没有Full GC只有YoungGC。但是G1不行,我们对G1的优化只能是尽可能的优化预定的停顿时间,其他的我们没法参与太多,因为它什么时候YoungGC我们都不确定。
    • G1的内存使用率是没有ParNew +CMS高的,G1有这么一个机制,如果G1的某一个Region存活对象达到了85%,那就不会去回收这个Region,但是那15%呢。如果是垃圾也回收不掉了。
    • G1的掌控性没有ParNew + CMS好,说白了就是心里没底。我们使用ParNew + CMS可以很确定多久YoungGC,对象增长速度等等等等吧,我们都能看到。但是G1什么时候垃圾回收我们都不知道,如果出现了内存泄漏,如果不是几个G的内存泄漏,我们也很难察觉出来。使用ParNew + CMS可以放心一些,不用搞个活动心惊胆战的。
  • 所以总结来说,如果是4核8G的机器,尽量还是用ParNew + CMS垃圾回收器,如果是大内存机器,就是用G1。
  • 然后说一下G1的原理吧,G1把堆内存平均分成了多个大小相同的Region,我们首先要设置堆内存的大小,然后G1会根据堆大小除以2048,分成2048个大小相同的Region。G1也是有年轻代、老年代的概念,但是只是概念。没有ParNew+CMS分的那么清楚。G1里的年轻代和老年代都是基于Region的,某些Region属于年轻代,某些Region属于老年代,由G1动态控制。但是现在属于年轻代的Region并不永远都是年轻代,如果年轻代的Region被回收了,下次这个Region可能就存放老年代的数据了。所以,G1的年轻代和老年代都是动态的,但是也有个上限。系统刚开始运行时,会给年轻代分配5%的Region来存放对象,年轻代最多可以占用60%的Region,这60%可以通过JVM参数指定,默认是60%,不过这个一般默认就好,如果达到了目标值,就会强制触发YoungGC
  • G1的年轻代也是分Eden和Survivor的,因为G1整体使用的都是复制回收算法。只是某些Region属于Eden,某些Region属于Survivor,系统新创建的对象会被分配到属于Eden的Region,如果垃圾回收就把存活对象复制到Survivor中。
  • G1的一个特点就是我们可以设置一个预期的停顿时间,也就是STW的时间,比如,某个系统的时效性要就特别高,每次GC我只允许STW的5ms,那我们就可以通过JVM参数设置成5ms的停顿,这样G1在垃圾回收时,就会把时间控制在5ms以内。
  • G1的垃圾回收不一定是年轻代满了,或者老年代满了才去回收。如果是那样,就和ParNew+CMS没区别了,大内存机器也要STW好久。G1是基于每个Region的性价比去回收的,比如,Region1里有20M对象,回收2ms,Region2里有50兆对象回收要4ms。如果我们设置系统停顿时间为5ms,那G1会在要求的时间内,尽可能回收更多的对象,它会选择Region2,因为性价比更高。所以,我们系统运行,一直往Eden放对象,如果G1觉得,此时回收一下垃圾,差不多要5ms,那可能G1就回去回收,不会等到年轻代占用60%才去回收。
  • G1中年轻代的对象什么情况下会进入老年代
    • 其实和ParNew + CMS整体上是差不多的,只有大对象的处理不一样
    • 1、YoungGC存活的对象Survivor放不下
    • 2、YoungGC存活的对象达到Survivor的50%,触发动态年龄审核
    • 3、对象到达了15岁,进入老年代
    • G1中大对象不会进入老年代,而是专门有一部分Region存放大对象用。如果一个Region放不下大对象,那就会横跨几个Region来存放。
  • G1的Old G也不是我们能控制的,G1会根据自己的判断觉得该回收的时候就会回收,不过也是基于复制算法的
  • G1的混合回收,如果老年代占比45%,就会触发混合回收,回收整个堆内存,但是混合回收也是会控制在我们设置的停顿时间的范围内的,如果时间不够,就会分多次回收。混合回收有点和CMS的回收类似
    • 第一步,初始标记
      • 初始标记需要STW,这一步只标记GC Root直接应用的对象,速度很快
    • 第二步,并发标记
      • 和系统并行,深入的追踪GC Root,标记所有存活的对象,此时系统新创建的对象会被JVM记录,这一步不需要STW
    • 第三步,重新标记,重新标记第二步有改动的对象,要STW。因为只有一小部分改动,速度很快
    • 第四步,混合回收,只有这一步和CMS不一样,CMS这里的回收时和系统并行的。但是G1的混合回收需要STW。混合回收不仅会回收老年代,还会回收新生代和大对象。如果一次性全回收掉,那时间就太久了,可能达不到我们设置的预期停顿时间,所以G1这里是分几批来回收的,回收一次,系统运行一会,然后再回收一次。JVM参数可以设置这个值,分几次去回收,默认值是8次,分8次回收。混合回收还有一个参数我们可以设置,就是空闲的Region达到百分之多少,停止回收,默认是5%。
  • G1何时会触发Full GC,其实G1的混合回收就相当于ParNew + CMS的Full GC了,因为回收了所有的区域,只不过回收时间可以控制在我们指定的范围内。但是G1的Full GC就没法控制了,可能要卡顿特别久才能回收完。什么情况下会出现呢,因为G1的整体是基于复制算法的,如果回收的过程中,发现存活对象找不到可以复制的Region,放不下了。那就Full GC,开始单线程标记、清理、整理空闲出一批Region,这个过程很慢。
  • 然后说一下G1的优化,G1比较智能,我们可以参与优化的点很少,我们只能合理的设置停顿时间,不要太小也不要太大,太小GC会太频繁,每秒都在GC。太大的话,停顿时间太久了也不好。
  • 平时我们选择垃圾回收器要根据不同的场景具体去分析,该使用那个。没有绝对的好坏。优化也没有一个统一的标准。比如YoungGC和Full GC多久一次好,YoungGC、Full GC耗时多久比较好。这个还是看系统的,只要不影响系统使用,没有卡顿感,我觉得都是好的。而且有些系统内部使用的,即使卡顿一会也无所谓,如果优化的话,用大内存机器成本也在那呢,不用做没必要的优化。

 

 

  • 说说平时工作怎么JVM调优的
    • 如果开发一个新系统,JVM的调优不是一次性就调完的,要分几次去看
      • 第一步,系统开发完需要自己预估一个JVM参数,也就是你预估每秒大概会有多少的对象进入,然后选几台机器,把内存比例设置的合理一些就好了,一般公司都会有一套公司级的通用JVM参数模板,如果是刚开发完,可以直接使用通用模板,反正测试环境还要压测
      • 第二步,测试环境系统压测,使用工具模拟1000人或几千人同时使用,造成每秒几百上千的请求压力,响应时间要控制在200ms。然后压测期间我们需要通过 jstat 去看下内存使用情况,就是我之前说的那些,什么Eden区增长速度,各个GC的频率啊,有没有内存泄漏情况等等吧。如果观察YoungGC和Full GC频率没什么问题,系统没有卡顿现象,就可以上线了。
      • 第三步,如果公司有监控系统,就持续监控,如果没有就每天高峰时期,通过jstat查看一下机器的JVM运行状态,如果需要优化,就继续优化

 jstat -gc PID 1000 1000

 

一、概述

此命令是实验性的,不受支持。

jstat -help|-options

jstat -<options> [-t] [-h<lines>] <vmid> [<interval> [<count>] ]
  • 1
  • 2
  • 3
  • -help 输出帮助信息。
  • -options 显示静态选项列表。请参见输出选项
  • -t 将时间戳列显示为输出的第一列。时间戳是自目标 JVM 开始时间以来的时间。
  • -h n 每 n 个样本(输出行)显示一个列标题,其中 n 是正整数。默认值为 0,显示第一行数据的列标题。
  • vmid 虚拟机标识符,它是指示目标 JVM 的字符串。请参见虚拟机标识符
  • interval 以指定单位、秒或毫秒为单位的采样间隔。默认单位是毫秒。这必须是正整数。指定后,jstat 命令会在每个间隔产生输出。
  • count 要显示的样本数。默认值为无穷大,这将导致 jstat 命令显示统计信息,直到目标 JVM 终止或 jstat 命令终止。该值必须是正整数。

二、说明

jstat 命令显示已检测的 Java HotSpot 虚拟机的性能统计信息。目标 JVM 由其虚拟机标识符或 vmid 选项来标识。
jstat 命令支持两种类型的选项,常规选项和输出选项。常规选项使 jstat 命令显示简单的用法和版本信息。输出选项决定统计输出的内容和格式。

在未来版本中,所有选项及其功能可能会有所更改或删除。

三、输出选项

 如果不指定常规选项,则可以指定输出选项。输出选项决定了 jstat 命令输出的内容和格式,并且由一个 statOption 和任何其他输出选项(-h、-t 和 -J)组成。必须首先关注统计。
 输出格式为表格,列之间用空格隔开。标题行描述了列。使用-h选项设置标题显示的频率。不同选项之间的列标题名称是一致的。通常,如果两个选项提供了同名的列,则这两列的数据源是相同的。
 使用 -t 选项显示时间戳列,标记时间戳作为输出的第一列。时间戳列包含自目标 JVM 启动以来经过的时间,以秒为单位。时间戳的分辨率取决于各种因素,并且会因重载系统上的延迟线程调度而发生变化。
 使用间隔和计数参数分别确定 jstat 令显示其输出的频率和次数。

不要编写脚本来解析 jstat 命令的输出,因为格式在将来的版本中可能会改变。如果您编写了解析 jstat 命令输出的脚本,那么应该为该工具的未来版本修改它们。

  • -statOption
     确定 jstat 命令显示的统计信息。以下列出了可用的选项。使用 -options 常规选项显示特定平台安装的选项列表。请参见统计选项和输出。
    序号统计项描述
    1 class 显示有关类加载器行为的统计信息
    2 compiler 显示有关 Java HotSpot 虚拟机即时编译器行为的统计信息
    3 gc 显示垃圾收集堆行为的统计信息
    4 gccapacity 显示各代容量及其相应空间的统计信息
    5 gccause 显示垃圾收集统计信息(与 -gcutil 相同)的摘要,以及上次和当前(如果适用)垃圾收集事件的原因
    6 gcnew 显示新一代行为的统计数据
    7 gcnewcapacity 显示关于新一代大小及其相应空间的统计信息
    8 gcold 显示老一代行为的统计信息和元空间统计信息
    9 gcoldcapacity 显示关于老一代大小的统计数据
    10 gcmetacapacity 显示关于元空间大小的统计信息
    11 gcutil 显示垃圾收集统计信息的摘要
    12 printcompilation 显示 Java HotSpot 虚拟机编译方法统计信息
  • -JjavaOption
     将 Java 选项传递给 Java 应用程序启动器。例如,-J-Xms48m 将启动内存设置为 48 MB。有关选项的完整列表,请参见 java

四、统计选项和输出

 以下信息总结了 jstat 命令为每个 statOption 输出的列。

  • -class option
     类装入器统计。
    序号统计项描述
    1 Loaded 加载的类的数目
    2 Bytes 加载的数量大小 KB
    3 Unloaded 卸载类数
    4 Bytes 卸载的数量大小 KB
    5 Time 执行类加载和卸载操作所花费的时间
  • -compiler option
     Java HotSpot 虚拟机即时编译器统计信息。
    序号统计项描述
    1 Compiled 执行编译任务数
    2 Failed 编译任务失败的次数
    3 Invalid 无效的编译任务数
    4 Time 执行编译任务花费的时间
    5 FailedType 上次编译失败的编译类型
    6 FailedMethod 上次编译失败的类名和方法
  • -gc option
     收集垃圾回收统计信息。
    序号统计项描述
    1 S0C 年轻代中第 1 个 survivor(幸存区)的容量上限(KB)
    2 S1C 年轻代中第 2 个 survivor(幸存区)的容量上限(KB)
    3 S0U 年轻代中第 1 个 survivor(幸存区)的已使用容量(KB)
    4 S1U 年轻代中第 2 个 survivor(幸存区)的已使用容量(KB)
    5 EC 年轻代中 Eden(伊甸园)空间容量上限(KB)
    6 EU 年轻代中 Eden(伊甸园)空间已使用容量(KB)
    7 OC 老年代空间容量上限(KB)
    8 OU 老年代已使用容量(KB)
    9 MC 元空间提交大小(KB)
    10 MU 元空间利用率(KB)
    11 CCSC 压缩类提交大小(KB)
    12 CCSU 使用的压缩类空间(KB)
    13 YGC 年轻代 GC 总次数
    14 YGCT 年轻代 GC 总耗时(秒)
    15 FGC 老年代 GC 总次数
    16 FGCT 老年代 GC 总耗时(秒)
    17 GCT GC 总耗时(秒)
  • -gccapacity option
     收集堆内存统计信息。
    序号统计项描述
    1 NGCMN 最小新生代容量(KB)
    2 NGCMX 最大新生代容量(KB)
    3 NGC 当前新生代容量(KB)
    4 S0C 当前幸存区 0 容量(KB)
    5 S1C 当前幸存区 1 容量(KB)
    6 EC 当前 eden 空间容量(KB)
    7 OGCMN 老年代最小容量(KB)
    8 OGCMX 老年代最大容量(KB)
    9 OGC 当前老年代容量(KB)
    10 OC 当前老年代大小(KB)
    11 MCMN 最小元空间容量(KB)
    12 MCMX 最大元空间容量(KB)
    13 MC 元空间提交大小(KB)
    14 CCSMN 压缩类空间最小容量(KB)
    15 CCSMX 压缩类空间最大容量(KB)
    16 CCSC 压缩类提交大小(KB)
    17 YGC 年轻代 GC 总次数
    18 FGC 老年代 GC 总次数
  • -gccause option
     此选项显示与 -gcutil 选项相同的垃圾收集统计信息摘要,但包括上次垃圾收集事件的原因和当前垃圾收集事件(如果适用)。除了为 -gcutil 列出的列之外,此选项还添加了以下列:
    序号统计项描述
    1 LGCC 上次垃圾收集的原因
    2 GCC 当前垃圾收集的原因
  • -gcnew option
     新生代垃圾回收统计信息。
    序号统计项描述
    1 S0C 当前幸存区 0 容量(KB)
    2 S1C 当前幸存区 1 容量(KB)
    3 S0U 当前幸存区 0 使用大小(KB)
    4 S1U 当前幸存区 0 使用大小(KB)
    5 TT 对象在新生代存活的次数
    6 MTT 对象在新生代存活的最大次数
    7 DSS 期望的幸存区大小(KB)
    8 EC eden 区的大小(KB)
    9 EU eden 区的使用大小(KB)
    10 YGC 年轻代 GC 总次数
    11 YGCT 年轻代 GC 总耗时(秒)
  • -gcnewcapacity option
     新生代内存信息统计。
    序号统计项描述
    1 NGCMN 新生代最小容量(KB)
    2 NGCMX 新生代最大容量(KB)
    3 NGC 当前新生代容量(KB)
    4 S0CMX 最大幸存 0 区大小(KB)
    5 S0C 当前幸存 0 区大小(KB)
    6 S1CMX 最大幸存 1 区大小(KB)
    7 S1C 当前幸存 1 区大小(KB)
    8 ECMX 最大 eden 区大小(KB)
    9 EC 当前 eden 区大小(KB)
    10 YGC 年轻代 GC 总次数
    11 FGC 老年代 GC 总次数
  • -gcold option
     老年代垃圾回收信息统计。
    序号统计项描述
    1 MC 方法区大小(KB)
    2 MU 方法区使用大小(KB)
    3 CCSC 压缩类空间大小(KB)
    4 CCSU 压缩类空间使用大小(KB)
    5 OC 老年代大小(KB)
    6 OU 老年代使用大小(KB)
    7 YGC 年轻代 GC 总次数
    8 FGC 老年代 GC 总次数
    9 FGCT 老年代 GC 总耗时(秒)
    10 GCT GC 总耗时(秒)
  • -gcoldcapacity option
     老年代内存信息统计。
    序号统计项描述
    1 OGCMN 老年代最小容量(KB)
    2 OGCMX 老年代最大容量(KB)
    3 OGC 当前老年代大小(KB)
    4 OC 老年代大小(KB)
    5 YGC 年轻代 GC 总次数
    6 FGC 老年代 GC 总次数
    7 FGCT 老年代 GC 总耗时(秒)
    8 GCT GC 总耗时(秒)
  • -gcmetacapacity option
     元数据空间信息统计。
    序号统计项描述
    1 MCMN 最小元数据容量
    2 MCMX 最大元数据容量
    3 MC 当前元数据空间大小
    4 CCSMN 最小压缩类空间大小
    5 CCSMX 最大压缩类空间大小
    6 CCSC 当前压缩类空间大小
    7 YGC 年轻代 GC 总次数
    8 FGC 老年代 GC 总次数
    9 FGCT 老年代 GC 总耗时(秒)
    10 GCT GC 总耗时(秒)
  • -gcutil option
     总结垃圾回收信息统计。
    序号统计项描述
    1 S0 幸存1区当前使用比例
    2 S1 幸存2区当前使用比例
    3 E Eden(伊甸园)空间容量使用比例
    4 O 老年代空间容量使用比例
    5 M 元数据区使用比例
    6 CCS 压缩使用比例
    7 YGC 年轻代 GC 总次数
    8 YGCT 年轻代 GC 总耗时(秒)
    9 FGC 老年代 GC 总次数
    10 FGCT 老年代 GC 总耗时(秒)
    11 GCT GC 总耗时(秒)
  • -printcompilation option
     JVM编译方法信息统计。
    序号统计项描述
    1 Compiled 最近编译方法的数量
    2 Size 最近编译方法的字节码数量
    3 Type 最近编译方法的编译类型
    4 Method 方法名标识

五、虚拟机标识符

 vmid 字符串的语法对应于URI的语法:

[protocol:][//]lvmid[@hostname[:port]/servername]
  • 1

 vmid 字符串的语法对应于 URI 的语法。vmid 字符串可以从表示本地 JVM 的简单整数变化到指定通信协议、端口号和其他特定于实现的值的更复杂的结构。

  • protocol
     通信协议。如果省略 protocol 值并且未指定主机名,则默认协议是特定于平台的优化本地协议。如果省略 protocol 值并指定主机名,则默认协议为 rmi。
  • lvmid
     目标 JVM 的本地虚拟机标识符。lvmid 是特定于平台的值,它唯一标识系统上的 JVM。lvmid 是虚拟机标识符唯一必需的组件。lvmid 通常是但不一定是目标 JVM 进程的操作系统进程标识符。如果 JVM 进程没有在单独的 docker 实例中运行,您可以使用 jps 命令来确定 lvmid。您还可以使用 ps 命令在 Oracle Solaris、Linux 和 macOS 平台上确定 lvmid,并使用窗口任务管理器在窗口上确定 lvmid。

    JDK 10 增加了在附加到运行在独立备审进程中的 Java 进程时使用附加应用编程接口的支持。但是,jps 命令不会列出在单独的 docker 实例中运行的 JVM 进程。如果您试图将一个 Linux 主机与一个位于 docker 容器中的虚拟机连接起来,您必须使用诸如 ps 之类的工具来查找 JVM 的 PID。

  • hostname
     指示目标主机的主机名或 IP 地址。如果省略 hostname 值,则目标主机是本地主机。
  • port
     与远程服务器通信的默认端口。如果 hostname 值被省略,或者协议值指定了优化的本地 protocol,则 port 值被忽略。否则,port 参数的处理是特定于实现的。对于默认 rmi 协议,端口值指示远程主机上 rmiprotocol 的端口号。如果 port 值被省略并且 protocol 值指示rmi,则使用默认 rmiregistry 端口(1099)。
  • servername
     servername 参数的处理取决于实现。对于优化的本地协议,此字段被忽略。对于 rmi 协议,它表示远程主机上 RMI 远程对象的名称。

六、示例

 本节介绍一些监控 lvmid 为 21891 的本地 JVM 的示例。

  • -gcutil 选项
     此示例将监控 lvmid 为 21891 的进程,并以 250 毫秒的间隔进行 7 个采样,并显示 -gcutil 选项指定的输出。

    jstat -gcutil 21891 250 7
    
    • 1

     输出结果为:

      S0     S1     E      O      M     CCS     YGC   YGCT    FGC    FGCT     GCT   
      0.00  97.02  70.31  66.80  95.52  89.14    7    0.300    0    0.000    0.300
      0.00  97.02  86.23  66.80  95.52  89.14    7    0.300    0    0.000    0.300
      0.00  97.02  96.53  66.80  95.52  89.14    7    0.300    0    0.000    0.300
      91.03 0.00   1.98   68.19  95.89  91.24    8    0.378    0    0.000    0.378
      91.03 0.00   15.82  68.19  95.89  91.24    8    0.378    0    0.000    0.378
      91.03 0.00   17.80  68.19  95.89  91.24    8    0.378    0    0.000    0.378
      91.03 0.00   17.80  68.19  95.89  91.24    8    0.378    0    0.000    0.378
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

     此示例的输出显示在第三和第四样本之间发生了年轻代收集。 收集耗时 0.078 秒,并将对象从 eden 空间(E)提升到旧空间(O),从而使旧空间利用率从 66.80% 提高到 68.19%。 在收集之前,幸存者空间的利用率为 97.02%,但是在收集之后,利用率为 91.03%。

  • 重复显示列标题
     此示例将监控 lvmid 为 21891 的进程,并以 250 毫秒的间隔进行采样,并显示 -gcnew 选项指定的输出。 另外,它使用 -h3 选项在每三行数据之后输出列标题。

    jstat -gcnew -h3 21891 250
    
    • 1

     输出结果为:

      S0C    S1C    S0U    S1U   TT  MTT  DSS    EC      EU     YGC    YGCT
      64.0   64.0   0.0    31.7  31  31   32.0   512.0   178.6  249    0.203
      64.0   64.0   0.0    31.7  31  31   32.0   512.0   355.5  249    0.203
      64.0   64.0   35.4   0.0   2   31   32.0   512.0   21.9   250    0.204
      S0C    S1C    S0U    S1U   TT  MTT  DSS    EC      EU     YGC    YGCT
      64.0   64.0   35.4   0.0   2   31   32.0   512.0   245.9  250    0.204
      64.0   64.0   35.4   0.0   2   31   32.0   512.0   421.1  250    0.204
      64.0   64.0   0.0    19.0  31  31   32.0   512.0   84.4   251    0.204
      S0C    S1C    S0U    S1U   TT  MTT  DSS    EC      EU     YGC    YGCT
      64.0   64.0   0.0    19.0  31  31   32.0   512.0   306.7  251    0.204
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

     除了显示重复的标题字符串外,此示例还显示在第二个和第三个样本之间,出现了年轻的 GC。 持续时间为 0.001 秒。 集合发现了足够的活动数据,以至于生存空间 0 利用率(S0U)将超过所需的生存空间大小(DSS)。 结果,对象被提升为旧一代(在此输出中不可见),并且任职期限(TT)从 31 降低为 2。
     在第五个和第六个样本之间发生另一个收集。 该集合发现的幸存者很少,并将任期阈值恢复为31。

  • 带时间戳输出
     此示例连接到 lvmid 21891,并以 250 毫秒的间隔进行 3 个采样。 -t 选项用于为第一列中的每个样本生成时间戳。

    jstat -gcoldcapacity -t 21891 250 3
    
    • 1

    输出结果为:

      Timestamp   OGCMN    OGCMX      OGC       OC       YGC   FGC    FGCT    GCT
      150.1       1408.0   60544.0   11696.0   11696.0   194    80    2.874   3.799
      150.4       1408.0   60544.0   13820.0   13820.0   194    81    2.938   3.863
      150.7       1408.0   60544.0   13820.0   13820.0   194    81    2.938   3.863
    
    • 1
    • 2
    • 3
    • 4

     “时间戳”列报告自目标 JVM 启动以来经过的时间(以秒为单位)。 此外,-gcoldcapacity 输出显示,随着堆扩展以满足分配或升级需求,当前老年代容量(OGC)和老年代容量(OC)会增加。 在老年代回收(FGC)第 80 次后,当前老年代容量(OGC)从 11696 KB 增加到 13820 KB。 生成的最大容量(和空间)为60544 KB(OGCMX),因此仍有扩展空间。

  • 监控远程JVM
     该示例使用 -gcutil 选项连接到名为 remote.domain 的系统上的 lvmid 40496,并无限期地采样。

    jstat -gcutil 40496@remote.domain 1000
    
    • 1

     lvmid 与远程主机的名称组合以构建 40496@remote.domain 的 vmid。 此 vmid 导致使用 rmi 协议与远程主机上的默认 jstatd 服务器进行通信。 jstatd 服务器使用 remote.domain 上的 rmiregistry 命令定位,该命令绑定到 rmiregistry 命令的默认端口(端口1099)。

posted @ 2024-01-09 11:15  dint  阅读(44)  评论(0编辑  收藏  举报