i-stat怎么打印从0开始成为JVM实战高手(三)

新闻资讯2026-04-20 23:47:55

jstat可以用来检查JVM的Eden、Survivor、老年代的内存使用情况,还有Young GC和Full gC的执行次数以及耗时。

jstat -gc PID

i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第1张

jstat -gc 89464 10000 30

jstat -gc PID 1000 10:每隔1秒钟更新出来最新的一行jstat统计信息,一共执行10次jstat统计
用于观察动态变化,上面1000指1秒,还可以调整成1分钟,如果负载低的系统的话

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的总耗时

其他的istat命令

1.jstat -gccapacity PID:堆内存分析
2.jstat -gcnew PID:年轻代GC分析,这里的TT和MTT可以看到对象在年轻代存活的年龄和存活的最大年龄
3.jstat -gcnewcapacity PID:年轻代内存分析
4.jstat -gcold PID:老年代GC分析
5.jstat -gcoldcapacity PID:老年代内存分析
6.jstat -gcmetacapacity PID:元数据区内存分析

jmap:了解系统运行时的内存区域
JVM新增对象的速度很快,看看到底什么对象占据了那么多的内存

jmap -heap PID 查看堆内存里一些基本各个区域的情况
jmap -histo PID 了解对象对内存占用的情况,快速了解当前内存到底是哪个对象占用了大量的内存空间

i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第2张
各种对象占用内存空间的大小降序排列,把占用内存最多的对象放在最上面

使用jmap生成堆内存转储快照
jmap -dump:live,format=b,file=dump.hprof PID
会在当前目录下生成一个dumn.hprof文件,这是二进制的格式

使用jhat在浏览器中分析堆转出快照
通过浏览器来以图形化的方式分析堆转储快照
通过如下命令即可启动jhat服务器,还可以指定自己想要的http端口号,默认是7000端口号
jhat -port 7000 dump.hprof

优化思路其实简单来说就一句话:尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少full GC的频率,避免频繁Full gc对JVM性能的影响

对线上系统进行JVM监控
第一种方法(low) ,每天在高峰期和低峰期都用jstat、jmap、jhat等工具去看看线上系统的JVM运行是否正常,有没有频繁Full GC的问题
第二种:专门的监控系统Zabbix,OpenFalcon,Ganglia,等等
然后部署的系统都可以把JVM统计项发送到监控系统

普通机器4核8G
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第3张
假设每秒500个请求,每个请求加载100kb数据,约50M,加载到内存中计算
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第4张
只要区区20秒,就会迅速填满Eden区。然后出发一次YoungGc对新生代进行回收,1G左右的Eden进行Young GC速度相对是比较快的,可能也就几十ms的时间。

模拟代码的JVM参数设置

-XX:NewSize=104857600         新生代初始化100M,意味着Eden80M,每块Survivor是10M,老年代也是100M
-XX:MaxNewSize=104857600         新生代最大大小100M,如果新生代的大小超过这个值,JVM将抛出OutOfMemoryError。
-XX:InitialHeapSize=209715200         堆初始化大小200M,堆是JVM用于存储对象的内存区域
-XX:MaxHeapSize=209715200          堆的最大大小200M。如果堆的大小超过这个值,JVM将抛出OutOfMemoryError.
-XX:SurvivorRatio=8          意味着Eden区的大小是Survivor区的8倍
-XX:MaxTenuringThreshold=15          对象在新生代中可以经历的最大GC次数,超过这个次数后就会被晋升到老年代
-XX:PretenureSizeThreshold=3145728          对象的大小阈值3M,超过这个值的对象直接在老年代中分配
-XX:+UseParNewGC          开关参数,用于启用并行的新生代垃圾收集器
-XX:+UseConcMarkSweepGC          开关参数,用于启用CMS垃圾收集器,这是一种以获取最小停顿时间为目标的收集器
-XX:+PrintGCDetails          开关参数,用于打印垃圾收集的详细信息到标准输出
-XX:+PrintGCTimeStamps          开关参数,用于打印每次垃圾收集发生的时间戳
-Xloggc:gc2.log          指定将垃圾收集日志输出到名为gc2.log的文件

示例程序

public class BiDemo1 {
    public static void main(String[] args) throws Exception {
        Thread.sleep(30000);   //休眠30秒
        while (true){
            loadData();
        }
    }

    private static void loadData() throws Exception {
        byte[] data = null;
        for (int i = 0; i < 50; i++) {
            data=new byte[100*1024];
        }
        data= null;
        Thread.sleep(1000);
    }
}

休眠30秒是让我们找到这个程序的PID,也就是进程ID,
然后再执行jstat命令来观察程序运行时的JVM状态
接着loadData()方法,循环50次,模拟每秒50个请求,然后每次请求会分配一个100kb的数组
,模拟每次请求会从数据存储中加载出来100kb的数据。接着会休眠1秒钟,模拟这一切都是
发生在1秒内的。
在main方法里有一个while(true)循环,模拟系统按照每秒钟50个请求,每个请求加载
100kb数据的方式不停的运行,除非我们手动终止程序。

windows上执行命令:https://gitforwindows.org/

通过jstat观察程序的运行状态
输入 jps 命令 ,找到对应该程序的ID,执行 jstat -gc 14344 1000 1000,意思就是针对这个进程统计jvm运行状态,同时每隔1秒钟打印一次统计信息,连续打印1000次。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第5张

这个系统就是会不停的从MySQL数据库以及其他数据源里提取大量的数据加载到自己的JVM内存里来进行计算处理。

这个数据计算系统会不停的通过SQL语句和其他方式从各种数据存储中提取数据到内存中来进行计算,大致当时的生产负载是每分钟大概需要执行500次数据提取和计算的任务。

但这是一套分布式运行的系统,所以生产环境部署了多台机器,每台机器大概每分钟负责执行100次数据提取和计算的任务。

每次会提取大概1万条左右的数据到内存里来计算,平均每次计算大概需要耗费10秒左右的时间。
然后每台机器是4核8G的配置,JVM内存给了4G,其中新生代和老年代分别是1.5G的内存空间,大家看下图。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第6张
每分钟会执行100次数据计算任务,每次是1万条数据需要计算10秒的时间,那么我们来看看每次1万条数据大概会占用多大的内存空间?
这里每条数据都是比较大的,大概每条数据包含了平均20个字段,可以认为平均每条数据在1KB左右的大小。
每次计算任务的1万条数据就对应了10MB的大小
如果新生代是按照8:1:1的比例来分配Eden和两块Survivor的区域,那么大体上来说,Eden区就是1.2GB,每块Survivor区域在100MB左右,如下图。

i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第7张
1分钟处理100个计算任务,一个计算任务1万条数据,一个数据1KB左右,1万数据就是10M,100个任务就是1000M,大约1分钟Eden区满了。

1.触发Minor GC的时候会有多少对象进入老年代?
执行Minor GC之前会先进行的检查,步骤如下:
首先第一步,先看看老年代的可用内存空间是否大于新生代全部对象?
刚开始Eden区只有1.2G,而老年代有1.5G,即使一次Minor GC过后,全部对象都存活,老年代也能放得下,那么此时就会直接执行Minor GC了。

那么此时Eden区里有多少对象还是存活的,无法被垃圾回收呢?
每个计算任务1万条数据需要计算10秒钟,所以假设此时80个计算任务都执行结束了,但是还有20个计算任务共计200MB的数据,还在计算中,那么此时就是200MB的对象是存活的,不能被垃圾回收掉,然后有1GB的对象是可以垃圾回收的,大家看下图。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第8张
此时一次Minor GC就会回收掉1GB的对象,然后200MB的对象能放入Survivor区吗?
不能!
因为任何一块Survivor区实际上就100MB的空间,此时就会通过空间担保机制,让这200MB对象直接进入老年代去,占用里面200MB内存空间,然后Eden区就清空了,大家看下图。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第9张
2.系统运行多久,老年代大概就会填满?
按照上述计算,每分钟都是一个轮回,大概算下来是每分钟都会把新生代的Eden区填满,然后触发一次Minor GC,然后大概都会有200MB左右的数据进入老年代。

假设现在2分钟运行过去了,此时老年代已经有400MB内存被占用了,只有1.1GB的内存可用,此时如果第3分钟运行完毕,又要进行Minor GC会做什么检查呢?

此时会先检查老年代可用空间是否大于新生代全部对象,此时老年代可用空间1.1GB,新生代对象有1.2GB,那么此时假设一次Minor GC过后新生代对象全部存活,老年代是放不下的,那么此时就得看看一个参数是否打开了 。

如果“-XX:-HandlePromotionFailure”参数被打开了,当然一般都会打开,此时会进入第二步检查,就是看看老年代可用空间是否大于历次Minor GC过后进入老年代的对象的平均大小。
我们已经计算过了,大概每分钟会执行一次Minor GC,每次大概200MB对象会进入老年代。
那么此时发现老年代的1.1GB空间,是大于每次Minor GC后平均200MB对象进入老年代的大小的,所以基本可以推测,本次Minor GC后大概率还是有200MB对象进入老年代,1.1G可用空间是足够的。

转折点大概在运行了7分钟过后,7次Minor GC执行过后,大概1.4G对象进入老年代,老年代剩余空间就不到100MB了,几乎快满了,如下图。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第10张
3.这个系统运行多久,老年代会触发1次Full GC?
大概在第8分钟运行结束的时候,新生代又满了,执行Minor GC之前进行检查,此时发现老年代只有100MB内存空间了,比之前每次Minor GC后进入老年代的200MB对象要小,此时就会直接触发一次Full GC。

Full GC会把老年代的垃圾对象都给回收了,假设此时老年代被占据的1.4G空间里,全部都是可以回收的对象,那么此时一次性就会把这些对象都给回收了,如下图。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第11张
然后接着就会执行Minor GC,此时Eden区情况,200MB对象再次进入老年代,之前的Full GC就是为这些新生代本次Minor GC要进入老年代的对象准备的,如下图。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第12张
按照这个运行模型,基本上平均就是七八分钟一次Full GC,这个频率就相当高了。因为每次Full GC速度都是很慢的,性能很差。

4.该案例应该如何进行JVM优化?
按照现有的内存模型,最大的问题,其实就是 每次Survivor区域放不下存活对象。

增加了新生代的内存比例,3GB左右的堆内存,其中2GB分配给新生代,1GB留给老年代,这样Survivor区大概就是200MB,每次刚好能放得下Minor GC过后存活的对象了,如下图所示。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第13张
只要每次Minor GC过后200MB存活对象可以放Survivor区域,那么等下一次Minor GC的时候,这个Survivor区的对象对应的计算任务早就结束了,都是可以回收的了,此时比如Eden区里1.6GB空间被占满了,然后Survivor1区里有200MB上一轮 Minor GC后存活的对象,如下图。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第14张
然后此时执行Minor GC,就会把Eden区里1.4GB对象回收掉,Survivor1区里的200MB对象也会回收掉,然后Eden区里剩余的200MB存活对象会放入Survivor2区里,如下图。
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第15张
以此类推,基本上就很少对象会进入老年代中,老年代里的对象也不会太多的。

通过这个分析和优化,定时我们成功的把生产系统的老年代Full GC的频率从几分钟一次降低到了几个小时一次,大幅度提升了系统的性能,避免了频繁Full GC对系统运行的影响。

5.运行程序用的示例JVM参数
下面的参数唯一修改的就是“-XX:PretenureSizeThreshold”,把大对象阈值修改为了20MB,避免我们程序里分配的大对象直接进入老年代。

-XX:NewSize=104857600 新生代初始化100M,Eden 80M,每块Survivor是10M,老年代也是100M
-XX:MaxNewSize=104857600 新生代最大大小100M,如果新生代的大小超过这个值,JVM将抛出OutOfMemoryError。
-XX:InitialHeapSize=209715200 堆初始化大小200M,堆是JVM用于存储对象的内存区域
-XX:MaxHeapSize=209715200 堆的最大大小200M。如果堆的大小超过这个值,JVM将抛出OutOfMemoryError.
-XX:SurvivorRatio=8 Eden区的大小是Survivor区的8倍
-XX:MaxTenuringThreshold=15 对象在新生代中可以经历的最大GC次数,超过这个次数后就会被晋升到老年代
-XX:PretenureSizeThreshold=20971520 对象的大小阈值20M,超过这个值的对象直接在老年代中分配
-XX:+UseParNewGC 开关参数,用于启用并行的新生代垃圾收集器
-XX:+UseConcMarkSweepGC 开关参数,用于启用CMS垃圾收集器,这是一种以获取最小停顿时间为目标的收集器
-XX:+PrintGCDetails 开关参数,用于打印垃圾收集的详细信息到标准输出
-XX:+PrintGCTimeStamps 开关参数,用于打印每次垃圾收集发生的时间戳
-Xloggc:gc3.log 指定将垃圾收集日志输出到名为gc3.log的文件

代码如下

public class JsDemo1 {
    public static void main(String[] args) throws Exception {
        Thread.sleep(30000);
        while (true){
            loadData();
        }
    }

    private static void loadData() throws Exception {
        byte[] data = null;
        for (int i = 0; i < 4; i++) {
            data=new byte[10*1024*1024];
        }
        data= null;

        byte[] data1=new byte[10*1024*1024];
        byte[] data2=new byte[10*1024*1024];

        byte[] data3=new byte[10*1024*1024];
        data3=new byte[10*1024*1024];

        Thread.sleep(1000);
    }
}

每秒钟都会执行一次loadData()方法,他会分配410MB的数组,但是都立马成为垃圾,
但是会有data1和data2两个10MB的数组是被变量引用必须存活的,
此时Eden区已经占用了六七十MB空间了,接着是data3变量依次指向了两个10MB的数组,
这是为了在1s内触发Young GC的。

6.基于jstat分析程序运行的状态
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第16张

程序运行后,突然在一秒就发生了一次Young GC,因为代码一次增加了80M,而survivor只有80M,对象还有未知对象信息,已然超过这个大小,Young GC过后S1区右846.6 kb,这
应该就是那些未知对象了。然后我们明显看到OU中多出来了30MB左右的对象,因此确定,在这次Young GC的时候,有30MB的对象存活,
此时因为Survivor区域放不下,所以直接进入老年代。
OU: 30722.1 -> 51204.2->61444.2 很明显每秒会发生一次Young GC,都会导致20MB~30MB左右的对象进入老年代,老年代的对象占用从30MB一路到60MB左右,此时突然在60MB之后下一秒,
明显发生了一次Full GC,对老年代进行了垃圾回收,因为此时老年代重新变成30MB了。
老年代总共就100MB左右,已经占用了60MB了,此时如果发生一次Young GC,有30MB存活对象要放入老年代的话,你还要放30MB对象,明显老年代就要不够了,
此时必须会进行Full GC,回收掉之前那60MB对象,然后再放入进去新的30MB对象
    
新生代对象增长的速率:每秒80M左右
Young GC的触发频率:1秒
Young GC的耗时: 30次YougGC下来 YGCT为0.099,平均每次花费3.3ms,可以观察FGC在相同次数下的YGCT间隔仅仅2毫秒,而不同的次数下YGCT达到10毫秒的也有。
老年代内存不够了触发Full GC,所以必须得等Full GC执行完毕了,Young GC才能把存活对象放入老年代,才算结束。这就导致Young GC也是速度非常慢。
每次Young GC存活对象大小:20~30MB
每次Young GC后对象进入老年代的大小:20~30MB
老年代对象增长的速率:每秒20~30MB
Full GC的触发频率:3秒
Full GC的耗时:14次Full GC用时0.009毫秒,0.6ms

7.对JVM性能进行优化
他最大的问题就是每次Young GC过后存活对象太多了,导致频繁进入老年代,频繁触发Full GC
只需要调大年轻代的内存空间,增加Survivor的内存即可,看如下JVM参数:

-XX:NewSize=209715200
-XX:MaxNewSize=209715200
-XX:InitialHeapSize=314572800
-XX:MaxHeapSize=314572800
-XX:SurvivorRatio=2
-XX:MaxTenuringThreshold=15
-XX:PretenureSizeThreshold=20971520
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:gc3.log

此时Eden区是100MB,每个Survivor区是50MB,老年代也是100MB。
接着用这个参数运行,用jstat来监控其运行状态如下:
i-stat怎么打印从0开始成为JVM实战高手(三)_https://www.jmylbn.com_新闻资讯_第17张

每秒的Young gC过后,都会有20MB左右的存活对象进入Survivor, 但是每个Survivor区都是50MB的大小,因此可以轻松容纳,
而且一般不会过50%的动态年龄判定的阈值。
    
每秒触发Yuong GC过后,几乎就没有对象会进入老年代,最终就736.7 KB的对象进入了
老年代里,其他就没有对象进入老年代了。
    
只有Young GC,没有Full GC,而且11次Young GC才不过44毫秒,平均一次GC 4毫秒都不到
,没有Full GC干扰之后,Young GC的性能极高。
    
所以,其实这个案例就优化成功了,同样的程序,仅仅是调整了内存分配比例,
立马就大幅度提升了JVM的性能,几乎把Full GC给消灭掉了。