JVM系列-调优实战案例:频繁FullGC和OOM案例

JVM调优是面试中常问的问题,同时也是实际工作中可能遇到的难题,本文简单介绍JVM调优在实战中的应用。

一、JVM调优的目标

在程序上线前,需要根据需求预估用户数和并发量,并按照这个目标对JVM进行规划和预调优;同时程序运行时间久了,可能会出现程序卡顿、访问变慢等情况;严重时会出现OOM导致程序崩溃。这些情况都需要进行JVM的调优。

调优的目标通常有两个:

  • 提高吞吐量(吞吐量=用户线程工作时间/(用户线程工作时间+GC垃圾回收时间))
  • 减少Stop-The-World的时间

二、调优相关的参数

JVM调优前,首先需要了解使用的是什么垃圾回收器。不同的垃圾回收器有不同的调优参数。随着JVM的发展,调优越来越简单,可能以后不再需要程序员对JVM进行调优。比如ZGC垃圾回收器,基本不用调优。

JDK使用的java虚拟机时Hotspot,JDK1.8使用的垃圾回收器默认是Parallel Scanvage + Parallel Old ( -XX:+UseParallelGC)。我们通常说的调优也是也是基于这两个垃圾回收器。

调优时要了解相关的命令行调优参数。 官方文档
Hotspot调优参数分类:

  • 标准参数: - 开头,所有的hotspot都支持。java命令可以直接查看。

  • 非标准参数: -X开头,特定版本的Hotspot支持。 java -X命令可以直接查看。

  • 不稳定参数: -XX开头,下个版本可能取消,但是很多实际调优都是使用-XX参数 。

打印所有的参数列表的方式:

  • 方式一

    [superuser@t-001 ~]$ java -XX:+PrintFlagsWithComments // debug版本才能用,可以自己编译一个Hotspot
    Error: VM option 'PrintFlagsWithComments' is notproduct and is available only in debug version of VM.
    Error: Could not create the Java Virtual Machine.
    Error: A fatal exception has occurred. Program will exit.
    [superuser@t-001 ~]$
    
  • 方式二

    [superuser@h-001 ~]$ java -XX:+PrintFlagsFinal
    
  • 方式三

    [superuser@h-001 ~]$ java -XX:+PrintFlagsInitial
    

查看java启动的默认参数:

[superuser@hb3-ack-test-kabu-001 ~]$ java -XX:+PrintCommandLineFlags
-XX:InitialHeapSize=524474496 -XX:MaxHeapSize=8391591936 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC

三、JDK自带的调优常用命令

常用命令:
jstack pid: 打印堆栈相关执行信息,可以用于死锁发现。
jinfo pid: 打印进程的启动详细参数、JVM正在使用的参数。
jstat -gc pid: 打印各个分代的内存使用情况。可以每个一段时间打印一次。
jvisualvm: windows图形化界面,不常用。上线之前内测可以使用。
jmap:   如果内存特别大,jmap会导致线程的停止,所以线上不能使用。
        线上时不能dump下来文件,因为文件可能很大(堆内存占了多少文件就有多大),dump时java程序会暂停,线上不能dump。
        jmap -histo pid | head -n 20 :查找有多少对象产生,及对象占用的内存,对象的名称。可用于内存泄露导致的OOM。

四、线上GC日志设置

合理的设置线上GC的日志文件,有助于出现问题时快速定位。通常设置多个(比如5个)日志文件,为了便于分析,每个文件不要特别大。日志会滚动写到文件,第五个写文件满了,会从第一个文件再写。也可以每天产生一个GC日志文件。

可以使用如下参数:

-Xloggc:/opt/xxxx/logs/xxxx-xx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause

五、好用的调优工具:Arthas

Arthas是阿里开源的JVM调优工具,非常好用,很多公司都使用这个工具。

Arthas提供了强大的调优功能,有很多好用的命令。JDK自带的调优命令arthas基本都实现了。

*       下载:curl -O https://arthas.aliyun.com/arthas-boot.jar  参考:https://arthas.aliyun.com/doc/install-detail.html#arthas-boot
*       运行:java -jar arthas-boot.jar
*       运行后可以:
*          1.可以看到检测到的进程编号,需要看哪个进程,直接输入进程编号。进入进程后,可以使用很多命令:
*              1.1.dashboard: 可以查看一个仪表盘,可以综合的简单查看这个进程相关的运行情况。
*              1.2.jvm: 类似jinfo命令,查询相关参数使用情况,包括使用的垃圾回收器是什么
*              1.3.thread: 把这个进程中的所有线程展示出来,这个很有用。和jstack有点像,但是比它好用。
*                     a.找到对应的线程后,使用 thread tid查看线程的详细执行情况。
*                     b.thread | grep XXX。过滤功能,所以建议线程要起名字。
*                     c.thread -b,可以直接找到有死锁的线程的名字
*          3.命令 -help : 查看具体命令的用法
*          4.查找类 sc; 查找方法 sm; 找到类名和方法名称后使用trace或monitor跟踪方法的整个运行情况,相关的统计。
*          5.heapdump: 类似jmap,可以将堆内存dump下来,使用工具分析。这个命令也会暂停程序,可以内测使用。
*          6.jad 类名: 反编译某个类,类似javap。分析线上运行的版本是不是最新的代码,是不是被别人提交的代码覆盖过。

六、频繁FullGC和OOM案例

 /**
 * 下面案例评估: 对数据库中的信用卡进行贷款评估。该程序执行一定时间后可能频繁FullGC,然后OOM。查询什么原因。
 * 启动参数:-XX:+PrintGC  -Xms200M -Xmx200M
 * @author 诸葛小猿
 * @date 2021-03-06
 */
public class JVMTest1 {
    /**
     * 线程池
     */
    public static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,new ThreadPoolExecutor.DiscardOldestPolicy());

    /**
     * main方法
     * @param args
     */
    public static void main(String[] args) throws Exception {
        executor.setMaximumPoolSize(50);

        // 死循环 每次去100条数
        for(;;){
            modFit();
            Thread.sleep(100);
        }
    }

    /**
     * 模型匹配
     */
    public static void modFit(){
        // 取100条数据
        List<CardInfo> list = getAllCard();

        list.forEach(cardInfo -> {
            // to do something
            executor.scheduleWithFixedDelay(()->{ cardInfo.m();},2,3, TimeUnit.SECONDS);
        });
    }

    /**
     * 每次从数据库中获得100个行用卡信息
     */
    public static List<CardInfo> getAllCard(){
        List<CardInfo> list = new ArrayList<>();

        for(int i=0; i<100; i++){
            list.add(new CardInfo());
        }
        return list;
    }

    /**
     * 模拟一张行用卡,
     * 里面有个空的m()
     */
    public static class CardInfo{
        BigDecimal price = new BigDecimal(0.0);
        String name = "张三";
        int age = 30;
        Date birthDate = new Date();

        public void m(){

        }
    }
}

七、案例分析

1.通过日志观察到的现象

  • 程序运行过程中使用的内存不断升高

  • FullGC出现的频率越来越高

  • 每次FullGC能回收的内存越来越少,最后基本回收不到内存

  • 最终出现OOM

2.原因分析

出现了内存泄露,一定是有的对象一直占着内存不释放,导致FullGC回收不了,最终OOM。那么是哪些对象呢?

通常线上不会很容易出现OOM,因为运维有监控,当内存使用到一定的阈值后,监控就会报警。就会及时定位问题,不会等到OOM时才处理。

3.定位代码

第一步:查询哪些实例占的内存比较大,实例数比较多。获取堆内存dump文件。

  • 方式一:使用jvm自带命令:jmap -histo pid | head -n 20 直接找到排名前20的对象。查找有多少对象产生,及对象占用的内存、对象的名称。也可以直接使用这个命令导出堆内存的dump文件。jmap会暂停程序,堆内存小时可以使用,通常线上不能直接使用。

  • 方式二:使用arthas的heapdump,导出堆内存的dump文件。这个命令也会暂停程序,通常线上不能直接使用。

  • 方式三:程序启动时设置参数:-XX:+HeapDumpOnOutOfMemoryError,程序oom后会自动生成dump文件。这种情况少,因为有监控,一般不会OOM。

第二步:使用相关的工具分析dump文件,找到占用内存使用最多的几个实例对象。比如:使用JDK自带的jvisualVM,导入dump文件进行分析。Dump文件比较大时,可能分析需要几天。

第三步:找到占用内存最多的对象后,分析他们在代码中的逻辑是什么,为什么产生了如此多的对象。

第四步:找到原因后,对代码进行优化。

关注公众号,输入“java-summary”即可获得源码。

完成,收工!

传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。

你可能感兴趣的:(JVM,JVM调优,实战,OOM,FullGC)