JVM性能调优(五):使用jmap和jhat摸清线上系统的对象分布

x33g5p2x  于2021-09-19 转载在 Java  
字(3.7k)|赞(0)|评价(0)|浏览(282)

写在前面

        jmap和jhat可以帮助我们观察线上JVM中的对象分布,了解到你的系统平时运行过程中,到底哪些对象占据了主角位置,他们占据了多少内存空间

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

        有时可能我们会发现JVM新增对象的速度很快,然后就想要去看看,到底什么对象占据了那么多的内存。

        如果发现有的对象在代码中可以优化一下创建的时机,避免那种对象对内存占用过大,那么也许甚至可以去反过来优化一下代码。
        当然,其实如果不是出现OOM那种极端情况,也并没有那么大的必要去着急优化代码。

先看一个命令:jmap -heap PID
这个命令可以打印出来一系列的信息,打印出来堆内存相关的一些参数设置,然后就是当前堆内存里的一些基本各个区域的情况 比如Eden区总容量、已经使用的容量、剩余的空间容量,两个Survivor区的总容量、已经使用的容量和剩余的空间容量,老年代的总容量、已经使用的容量和剩余的容量。

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

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

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

        他会按照各种对象占用内存空间的大小降序排列,把占用内存最多的对象放在最****上面
所以如果你只是想要简单的了解一下当前jvm中的对象对内存占用的情况,只要直接用jmap -histo命令即可,非常好用。你可以快速了解到当前内存里到底是哪个对象占用了大量的内存空间。

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

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

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

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

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

        使用如下命令即可启动jhat服务器,还可以指定自己想要的http端口号,默认是7000端口号:
jhat dump.hprof -port 7000 

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

5、案例实战

5.1、模拟代码的JVM参数设置

接着我们会用一段程序来模拟出那种频繁Full GC的一个场景,此时JVM参数如下所示:

-XX:NewSize=104857600 -XX:MaxNewSize=104857600 -XX:InitialHeapSize=209715200 
-XX:MaxHeapSize=209715200 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 
-XX:PretenureSizeThreshold=20971520 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log

“-XX:PretenureSizeThreshold”,把大对象阈值修改为了20MB,避免我们程序里分配的大对象直接进入老年代。

5.2、示例程序

public class Demo {

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

    public static void loadData() throws InterruptedException {
        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()方法,他会分配4个10MB的数组,但是都立马成为垃圾,但是会有data1和data2 两个10MB的数组是被变量引用必须存活的,此时Eden区已经占用了六七十MB空间了,接着是data3变量依次指向了两个10MB的数 组,这是为了在1s内触发Young GC的。

5.3、通过jstat观察程序的运行状态


        程序运行起来之后,突然在一秒内就发生了一次Young GC,这是为什么呢?
很简单,按照我们上述的代码,他一定会在一秒内触发一次Young GC的。Young GC过后,我们发现S1U,也就是一个Survivor区中有79B3K的存活对象,这应该就是那些未知对象了。

然后我们明显看到在OU中多出来了30MB左右的对象,因此可以确定,在这次Young GC的时候,有30MB的对象存活了,此时因为Survivor区域放不下,所以直接进入老年代了。

        很明显每秒会发生一次Young GC,都会导致20MB~30MB左右的对象进入老年代,因为每次Young GC都会存活下来这么多对象,但是Survivor区域是放不下的,所以都会直接进入老年代。
此时看到老年代的对象占用从30KB一路到60MB左右,此时突然在60MB之后下一秒,明显发生了一次Full GC,对老年代进行了垃圾回收,因为此时老年代重新变成30MB了。

        几乎是每秒新增80MB左右,触发每秒1次Young GC,每次Young GC后存活下来20MB~30MB的对象,老年代每秒新增20MB~30MB的对象,触发老年代几乎三秒一次Full GC,Young GC太频繁,而且每次GC后存活对象太多,频繁进入老年代,频繁触发老年代的GC。

对于YoungGC和FullGC的耗时?

18次Young GC,结果耗费了170毫秒,平均下来一次Young GC要5毫秒左右。但是9次Full GC才耗费10毫秒,平均下来一次Full GC才耗费1毫秒。这是为什么呢?
很简单,按照上述程序,每次Full GC都是由Young GC触发的,因为Young GC过后存活对象太多要放入老年代,老年代内存不够了触发Full GC,所以必须得等Full GC执行完毕了,Young GC才能把存活对象放入老年代,才算结束。这就导致Young GC也是速度非常慢。

5.4**、对JVM性能进行优化**

最大的问题就是每次Young GC过后存活对象太多了,导致频繁进入老年代,频繁触发Full GC。

我们只需要调大年轻代的内存空间,增加Survivor的内存即可,看如下JVM参数:

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

我们把堆大小调大为了300MB,年轻代给了200MB,同时“-XX:SurvivorRatio=2”表明,Eden:Survivor:Survivor的比例为2:1:1,
所以Eden区是100MB,每个Survivor区是50MB,老年代也是100MB。

接着我们用这个JVM参数运行程序,用jstat来监控其运行状态如下:

         在上述截图里,大家可以清晰看到,每秒的Young gC过后,都会有20MB左右的存活对象进入Survivor,但是每个Survivor区都是50MB的大小,因此可以轻松容纳,而且一般不会过50%的动态年龄判定的阈值。

我们可以清晰看到每秒触发Yuong GC过后,几乎就没有对象会进入老年代,最终就600KB的对象进入了老年代里,其他就没有对象进入老年代了。

再看下面的截图:

我们可以看到,只有Young GC,没有Full GC,而且11次Young GC才不过9毫秒,平均一次GC1毫秒都不到,没有Full GC干扰之后, Young GC的性能极高。 所以,其实这个案例
        所以,其实这个案例就优化成功了,同样的程序,仅仅是调整了内存分配比例,立马就大幅度提升了JVM的性能,几乎把Full GC给消灭掉了。

相关文章