使用 jstat 摸清线上系统的JVM运行状况

功能强大的jstat

它可以轻易的让你看到当前运行中的系统,他的JVM内的Eden、Survivor、老年代的内存使用情况,还有Young GC和Full gC的执行次数以及耗时。
通过这些指标,我们可以轻松的分析出当前系统的运行情况,判断当前系统的内存使用压力以及GC压力,还有就是内存分配是否合理。下面我们就一点点来看看这个jstat工具的使用。

jstat -gc PID

运行这个命令之后会看到如下列,给大家解释一下:

参数

说明

S0C

这是From Survivor区的大小

S1C

这是To Survivor区的大小

S0U

这是From Survivor区当前使用的内存大小

S1U

这是To Survivor区当前使用的内存大小

EC

这是Eden区的大小

EU

这是Eden区当前使用的内存大小

OC

这是老年代的大小

OU

这是老年代当前使用的内存大小

MC

这是方法区(永久代、元数据区)的大小

MU

这是方法区(永久代、元数据区)的当前使用的内存大小

YGC

这是系统运行迄今为止的Young GC次数

YGCT

这是Young GC的耗时

FGC

这是系统运行迄今为止的Full GC次数

FGCT

这是Full GC的耗时

GCT

这是所有GC的总耗时

jstat命令其他参数

除了上面的jstat -gc命令是最常用的以外,他还有一些命令可以看到更多详细的信息,如下所示:

命令

解释

jstat -gccapacity PID

堆内存分析

jstat -gcnew PID

年轻代GC分析,这里的TT和MTT可以看到对象在年轻代存活的年龄和存活的最大年龄

jstat -gcnewcapacity PID

年轻代内存分析

jstat -gcold PID

老年代GC分析

jstat -gcoldcapacity PID

老年代内存分析

jstat -gcmetacapacity PID

元数据区内存分析

到底该如何使用jstat工具

接着教教大家一些jstat工具使用的小技巧,先明确一下,我们分析线上的JVM进程,最想要知道的信息有哪些?
包括如下:新生代对象增长的速率,Young GC的触发频率,Young GC的耗时,每次Young GC后有多少对象是存活下来的,每次Young GC过后有多少对象进入了老年代,老年代对象增长的速率,Full GC的触发频率,Full GC的耗时。
只要知道了这些信息,其实我们就可以结合之前几周的文章分析过的JVMGC优化的方法,合理分配内存空间,尽可能让对象留在年轻代不进入老年代,避免发生频繁的Full GC。这就是对JVM最好的性能优化了!

新生代对象增长的速率

在线上linux机器上运行如下命令:jstat -gc PID 1000 10
这行命令,他的意思就是每隔1秒钟更新出来最新的一行jstat统计信息,一共执行10次jstat统计
通过这个命令,你可以非常灵活的对线上机器通过固定频率输出统计信息,观察每隔一段时间的jvm中的Eden区对象占用变化。
比如给大家举个例子,执行这个命令之后,第一秒先显示出来Eden区使用了200MB内存,第二秒显示出来的那行统计信息里,发信Eden区使用了205MB内存,第三秒显示出来的那行统计信息里,发现Eden区使用了209MB内存,以此类推。
此时你可以轻易的推断出来,这个系统大概每秒钟会新增5MB左右的对象。
而且这里大家可以根据自己系统的情况灵活多变的使用,比如你们系统负载很低,不一定每秒都有请求,那么可以把上面的1秒钟调整为1分钟,甚至10分钟,去看你们系统每隔1分钟或者10分钟大概增长多少对象。
还有就是一般系统都有高峰和日常两种状态,比如系统高峰期用的人很多,此时你就应该在系统高峰期去用上述命令看看高峰期的对象增长速率。然后你再得在非高峰的日常时间段内看看对象的增长速率。
按照上述思路,基本上你可以对线上系统的高峰和日常两个时间段内的对象增长速率有很清晰的了解。

Young GC的触发频率和每次耗时

其实多久触发一次Young GC就很容易推测出来了,因为系统高峰和日常时候的对象增长速率你都知道了,那么非常简单就可以推测出来高峰期多久发生一次Young GC,日常期多久发生一次Young GC。
比如你Eden区有800MB内存,那么发现高峰期每秒新增5MB对象,大概高峰期就是3分钟会触发一次Young GC。日常期每秒新增0.5MB对象,那么日常期大概需要半个小时才会触发一次Young GC。
那么每次Young GC的平均耗时呢?
简单,之前给大家说过,jstat会告诉你迄今为止系统已经发生了多少次Young GC以及这些Young GC的总耗时。
比如系统运行24小时后共发生了260次Young GC,总耗时为20s。那么平均下来每次Young GC大概就耗时几十毫秒的时间。
你大概就知道每次Young GC的时候会导致系统停顿几十毫秒。

每次Young GC后有多少对象是存活和进入老年代

其实每次Young GC过后有多少对象会存活下来,这个没法直接看出来,但是有办法可以大致推测出来。
之前我们已经推算出来高峰期的时候多久发生一次Young GC,比如3分钟会有一次Young GC
那么此时我们可以执行下述jstat命令:jstat -gc PID 180000 10。这就相当于是让他每隔三分钟执行一次统计,连续执行10次。
此时大家可以观察一下,每隔三分钟之后发生了一次Young GC,此时Eden、Survivor、老年代的对象变化。
正常来说,Eden区肯定会在几乎放满之后重新变得里面对象很少,比如800MB的空间就使用了几十MB。Survivor区肯定会放入一些存活对象,老年代可能会增长一些对象占用。所以这里的关键,就是观察老年代的对象增长速率。
从一个正常的角度来看,老年代的对象是不太可能不停的快速增长的,因为普通的系统其实没那么多长期存活的对象。如果你发现比如每次Young GC过后,老年代对象都要增长几十MB,那很有可能就是你一次Young GC过后存活对象太多了。
存活对象太多,可能导致放入Survivor区域之后触发了动态年龄判定规则进入老年代,也可能是Survivor区域放不下了,所以大部分存活对象进入老年代。
最常见的就是这种情况。如果你的老年代每次在Young GC过后就新增几百KB,或者几MB的对象,这个还算情有可缘,但是如果老年代对象快速增长,那一定是不正常的。
所以通过上述观察策略,你就可以知道每次Young GC过后多少对象是存活的,实际上Survivor区域里的和进入老年代的对象,都是存活的。
你也可以知道老年代对象的增长速率,比如每隔3分钟一次Young GC,每次会有50MB对象进入老年代,这就是年代对象的增长速率,每隔3分钟增长50MB。

Full GC的触发时机和耗时

只要知道了老年代对象的增长速率,那么Full GC的触发时机就很清晰了,比如老年代总共有800MB的内存,每隔3分钟新增50MB对象,那么大概每小时就会触发一次Full GC。
然后可以看到jstat打印出来的系统运行起劲为止的Full GC次数以及总耗时,比如一共执行了10次Full GC,共耗时30s,每次Full GC大概就是需要耗费3s左右。

使用jmap和jhat摸清线上系统的对象分布

使用jmap了解系统运行时的内存区域

有的时候可能我们会发现JVM新增对象的速度很快,然后就想要去看看,到底什么对象占据了那么多的内存。
如果发现有的对象在代码中可以优化一下创建的时机,避免那种对象对内存占用过大,那么也许甚至可以去反过来优化一下代码。
当然,其实如果不是出现OOM那种极端情况,也并没有那么大的必要去着急优化代码。
但是这篇文章我们来学习一下如何了解线上系统jvm中的对象分布,也是有好处的,比如之前我们在上周的案例中就发现年轻代里总是有500kb左右的未知对象,大家是不是会很好奇?如果可以看到jvm中这500kb的对象到底是什么就好了,所以学习一下这个技巧是有用的。
先看一个命令:

jmap -heap PID

这个命令可以打印出来一系列的信息,我们就不长篇大论的粘贴出来具体的信息了,因为内容篇幅太大了,其实也没太大意义,因为里面的东西大家自己看字面意思都能看懂的。我们就简单给大家说一下这里会打印出来什么东西。
大致来说,这个信息会打印出来堆内存相关的一些参数设置,然后就是当前堆内存里的一些基本各个区域的情况
比如Eden区总容量、已经使用的容量、剩余的空间容量,两个Survivor区的总容量、已经使用的容量和剩余的空间容量,老年代的总容量、已经使用的容量和剩余的容量。
但是这些信息大家会想了,其实jstat已经有了啊!对的,所以一般不会用jmap去看这些信息,毕竟他信息还没jstat全呢,因为没有gc相关的统计。

使用jmap了解系统运行时的对象分布

其实jmap命令比较有用的一个使用方式,是如下的:

jmap -histo PID

这个命令会打印出来类似下面的信息:

内存分析工具有哪些java_老年代


他会按照各种对象占用内存空间的大小降序排列,把占用内存最多的对象放在最上面。

所以如果你只是想要简单的了解一下当前jvm中的对象对内存占用的情况,只要直接用jmap -histo命令即可,非常好用

你可以快速了解到当前内存里到底是哪个对象占用了大量的内存空间。

使用jmap生成堆内存转储快照

但是如果你仅仅只是看一个大概,感觉就只是看看上述那些对象占用内存的情况,感觉还不够,想要来点深入而且仔细点的
那就可以用jmap命令生成一个堆内存快照放到一个文件里去,用如下的命令即可:

jmap -dump:live,format=b,file=dump.hprof PID

这个命令会在当前目录下生成一个dump.hrpof文件,这里是二进制的格式,你不能直接打开看的,他把这一时刻JVM堆内存里所有对象的快照放到文件里去了,供你后续去分析。

使用jhat在浏览器中分析堆转出快照

接着就可以使用jhat去分析堆快照了,jhat内置了web服务器,他会支持你通过浏览器来以图形化的方式分析堆转储快照
使用如下命令即可启动jhat服务器

jhat dump.hprof

接着你就在浏览器上访问当前这台机器的7000端口号,就可以通过图形化的方式去分析堆内存里的对象分布情况了。

从测试到上线:如何分析JVM运行状况及合理优化

开发好系统之后的预估性优化

就是自行估算系统每秒大概多少请求,每个请求会创建多少对象,占用多少内存,机器应该选用什么样的配置,年轻代应该给多少内存,Young GC触发的频率,对象进入老年代的速率,老年代应该给多少内存,Full GC触发的频率。
这些东西其实是可以根据你自己写的代码,大致合理的预估一下的。在预估完成之后,就可以采用之前多个案例介绍的优化思路,先给自己的系统设置一些初始性的JVM参数
比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值,等
优化思路其实简单来说就一句话:尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。

系统压测时的JVM优化

从本地的单元测试,到系统集成测试,再到测试环境的功能测试,预发布环境的压力测试,要保证系统的功能全部正常
而且在一定压力下性能、稳定性和并发能力都正常,最后才会部署到生产环境运行。
这里非常关键的一个环节就是预发布环境的压力测试,通常在这个环节,会使用一些压力测试工具模拟比如1000个用户同时访问系统,造成每秒500个请求的压力,然后看系统能否支撑住每秒500请求的压力。同时看系统各个接口的响应延时是否在比如200ms之内,也就是接口性能不能太慢,或者是在数据库中模拟出来百万级单表数据,然后看系统是否还能稳定运行。
然后根据压测环境中的JVM运行状况,如果发现对象过快进入老年代,可能是因为年轻代太小导致频繁Young GC,然后Young GC的时候很多对象还是存活的,结果Survivor也太小,导致很多对象频繁进入老年代。当然也可能是别的什么原因。

大家记住一点:真正的优化,必须是你根据自己的系统,实际观察之后,然后合理调整内存分布,根本没什么固定的JVM优化模板。

对线上系统进行JVM监控

当你的系统上线之后,你就需要对线上系统的JVM进行监控,这个监控通常来说有两种办法。
第一种方法会“low”一些,其实就是每天在高峰期和低峰期都用jstat、jmap、jhat等工具去看看线上系统的JVM运行是否正常,有没有频繁Full GC的问题。
如果有就优化,没有的话,平时每天都定时去看看,或者每周都去看看即可。

第二种方法在中大型公司里会多一些,大家都知道,很多中大型公司都会部署专门的监控系统,比较常见的有Zabbix、OpenFalcon、Ganglia,等等。
然后你部署的系统都可以把JVM统计项发送到这些监控系统里去。
此时你就可以在这些监控系统可视化的界面里,看到你需要的所有指标,包括你的各个内存区域的对象占用变化曲线,直接可以看到Eden区的对象增速,还会告诉你Young GC发生的频率以及耗时,包括老年代的对象增速以及Full GC的频率和耗时。
而且这些工具还允许你设置监控。也就是说,你可以指定一个监控规则,比如线上系统的JVM,如果10分钟之内发生5次以上Full GC,就需要发送报警给你。比如发送到你的邮箱、短信里,这样你就不用自己每天去看着了。
但是这些监控工具的使用不在我们专栏范畴里,因为这些内容并不一定每个公司都一样,也不一定每个公司都有
大家如果有兴趣,完全可以自行百度学习,比如“OpenFalcon监控JVM”,会看到很多资料。

简单一句话总结:对线上运行的系统,要不然用命令行工具手动监控,发现问题就优化,要不然就是依托公司的监控系统进行自动监控,可视化查看日常系统的运行状态。