JVM手动调优的完整过程(包含cpu飙升、OOM问题定位等详细步骤)二

前置知识补充关于线程池的使用,有问题的同学可以查看上篇博客内容:
https://blog.csdn.net/ZZJST/article/details/108187505

二、jvm调优

本节内容主要包含以下三个部分:
1、垃圾回收器大致介绍;
2、常用的jvm调优工具介绍、安装、使用;
3、实战案例分析、定位cpu飙升、OOM问题;

1、垃圾回收器大致介绍:

(1)常用垃圾回收器,如下图所示:

在这里插入图片描述
横线上方代表新生代的垃圾回收器,下方代表老年代的垃圾回收器!两者之间有直线相连的表示常用的组合方案!

(2)分代回收堆内存占比:
目前常用的商用垃圾收集器都使用的是分代垃圾回收方式。

分代垃圾回收器把内存分为:新生代(Young Generation)和老生代(Tenured Generation),如下图所示

在这里插入图片描述
好了关于垃圾回收器这块内容大致就介绍这么多!

2、常用的jvm调优工具介绍、安装、使用

(1)阿里开源工具arthas
官方文档地址:
https://alibaba.github.io/arthas/

下载:
访问上述地址:
在这里插入图片描述
将下载的全量arthas-packaging-3.3.9-bin.zip包上传至服务器,并解压到自定义目录:
在这里插入图片描述
如图,我将zip包解压到arthas目录,解压后内容结构如上图!
此工具不需要开放任何额外端口!因此非常安全!

(2)GCeasy
这是一款在线的gc日志分析工具!
https://gceasy.io/

如图:
在这里插入图片描述
选择文件后,点击分析按钮,就会在线分析出结果!

(3)jvisualvm

这是jdk自带的jvm分析工具!具体位置如下:
在这里插入图片描述
jdk安装的bin目录下!双击打开即可!

3、实战案例分析、定位cpu飙升、OOM问题

(1)案例一,测试代码如下:

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class T15_FullGC_Problem01 {
    private static class CardInfo {
        BigDecimal price = new BigDecimal(0.0);
        String name = "张三";
        int age = 5;
        Date birthdate = new Date();

        public void m() {}
    }

    private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
            new ThreadPoolExecutor.DiscardOldestPolicy());

    public static void main(String[] args) throws Exception {
        executor.setMaximumPoolSize(50);

        for (;;){
            modelFit();
            Thread.sleep(100);
        }
    }

    private static void modelFit(){
        List<CardInfo> taskList = getAllCardInfo();
        taskList.forEach(info -> {

            // do something
            executor.scheduleWithFixedDelay(() -> {
                //do sth with info
                info.m();

            }, 2, 3, TimeUnit.SECONDS);
        });
    }

    private static List<CardInfo> getAllCardInfo(){
        List<CardInfo> taskList = new ArrayList<>();

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

        return taskList;
    }
}

注意此代码块是去除 package信息的,否则会出现linux情况下编译后无法运行的情况!

1)查看当前版本jdk使用的默认垃圾回收器:

java -XX:+PrintCommandLineFlags -version

此命令只支持在windows下查看!Linux无法使用!

在这里插入图片描述
在这里插入图片描述
对比发现,jdk1.8使用的是PS + SerialOld组合垃圾回收器!

2)将代码上传至服务器自定义目录,javac编译后如下(此步骤是在安装了jdk情况下:):
在这里插入图片描述
3)启动程序:

java -Xms200M -Xmx200M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/taosun/logs/ -Xloggc:/opt/taosun/logs/tao-sun-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause T15_FullGC_Problem01
-Xms<size>        设置初始 Java 堆大小
-Xmx<size>        设置最大 Java 堆大小
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/taosun/logs/      OOM时在指定目录下生成dump文件
-Xloggc:/opt/taosun/logs/tao-sun-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause     生成gc的log文件(5个文件,每个大小为20M等log文件内容信息)

4)使用top命令观察内存、cup使用情况:
在这里插入图片描述
通过观察top命令,我们发现pid 为1414的java进程的cup使用率在持续不断提高(可能此时运维大哥已经电话警告你了)!
面对这种情况我们该怎么排查呢?
(1)首先面对cup、内存在持续不断的升高,首先我们第一步因该检查程序是否发生死锁:
使用arthas工具:
进入arthas解压目录,启动arthas:

java -jar arthas-boot.jar

在这里插入图片描述
如图可以观察到目前运行的所有java进程,根据进程id,选择前面的序号,直接回车(此时我们的pid=1414,序号为1,直接1,然后回车),出现下图表示attach成功:
在这里插入图片描述
arthas内部命令有很多,在这里我们可以直接通过help命令直接显示全部:
在这里插入图片描述
每个命令都有对应的功能解释,这里我们主要使用thread命令,再次使用thread --help,可以展示thread更详细的命令:
在这里插入图片描述

使用thread命令:
在这里插入图片描述
如图直接展示了当前进程下所有的线程信息,并根据线程的cpu、内存使用进行了倒序!我们可以直接找到使用占用最大的线程!

使用thread -b命令,直接查看死锁情况:
在这里插入图片描述
可以看到我们程序中并无死锁!
当我们按照规范创建使用线程池时,这里的信息对我们来说是非常有用的,因为我们可以通过线程池的信息,直接定位到问题代码!

本案例没有使用,因此无法在此直接定位问题!

使用dashboard命令,来查看我们当前堆内存的使用情况:
在这里插入图片描述
如图详细展示了,新生代、老年代的内存详细使用情况!
以上就是使用arthas查看线程问题的步骤!

接着我们通过jdk的原生命令来协助定位问题:
jstat -gc pid (刷新时间) -------->查看当前进程的gc情况:
在这里插入图片描述
主要查看当前gc的情况!
YGC youngGC的次数
YGCT youngGC时间
FGC fullGC次数
FGCT fullGC时间
GCT gc总时间

分析:
若此时频繁发生FGC,表示程序在不断的产生新的对象,且年轻代无法回收,最终对象经过一次次YGC后最终全部存储到老年代内存,且无法被回收!要知道发生FGC时,程序会出现STW暂停,当内存特别大时,每次FGC的时间都会很长,若频繁发生FGC可想而知对我们的程序产的影响!

接下来我们可以在程序启动时指定的日志目录中查看gc日志信息:
在这里插入图片描述
hprof结尾的表示OOM时产生的dump文件
current结尾的表示gc文件

下载最新的current文件使用gceasy进行分析!
产生的效果图:
在这里插入图片描述
主要展示了内存使用,gc暂停时间等信息!该部分主要详细展示了gc情况!

若此时目录中未产生hprof文件,我们需要在集群中挑选一台来进行jmap分析!注意此命名会导致堆暂停,因此需要在集群的环境下选择一台定位问题!
在这里插入图片描述
使用

jmap -histo pid |head 20

来观察进程中对象的创建情况!(由于一个进程中对象信息过多,所以我们指定观察前多少个)
在这里插入图片描述
如图,挑选出对我们有用的信息分析!
T15_FullGC_Problem01$CardInfo

T15_FullGC_Problem01类中CardInfo对象被创建了416300个实例,通过循环执行jmap -histo pid |head 20命令发现它的实例还在不断的增加!这里肯定是有问题的,这样循环增加下去最终肯定会内存溢出!至此们定位到此处代码,然后本地代码分析!

好的下面我们来简单分析下这段代码:

public class T15_FullGC_Problem01 {
    private static class CardInfo {
        BigDecimal price = new BigDecimal(0.0);
        String name = "张三";
        int age = 5;
        Date birthdate = new Date();

        public void m() {}
    }

    private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
            new ThreadPoolExecutor.DiscardOldestPolicy());

    public static void main(String[] args) throws Exception {
        executor.setMaximumPoolSize(50);

        for (;;){
            modelFit();
            Thread.sleep(100);
        }
    }

    private static void modelFit(){
        List<CardInfo> taskList = getAllCardInfo();
        taskList.forEach(info -> {

            // do something
            executor.scheduleWithFixedDelay(() -> {
                //do sth with info
                info.m();

            }, 2, 3, TimeUnit.SECONDS);
        });
    }

    private static List<CardInfo> getAllCardInfo(){
        List<CardInfo> taskList = new ArrayList<>();

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

        return taskList;
    }
}

(1)首先我们通过ScheduledThreadPoolExecutor创建了线程池,指定了核心线程大小、最大线程池大小、拒绝策略;
(2)for循环通过线程池来完成任务的定时调用;
a.每次for循环时,都会通过,getAllCardInfo方法创建100个CardInfo对象;
b.然后通过线程池去执行这100个体对象的方法,注意这是个定时任务,即程序启动后,每隔3秒钟,执行一次CardInfo的方法,这就会 导致被创建的对象永远无法被垃圾回收器回收;
(3)此线程池中使用的是默认的DelayedWorkQueue队列,这是一个无界队列,当核心线程数创建到50时,后续任务将会被不断的扔到此队列中去!

综上情况!
程序执行开始时CardInfo被不断创建,并不断创建线程去执行任务,当线程数到达50后,由于队列是无限队列,后续任务将会被全部扔到延迟队列中!所以最终的结果是:程序运行后cup和内存使用率在不断上升,年轻代发生YGC,老年代发生FGC,因为对象无法被回收,最终导致FGC频繁发生(即使发生也无法回收到足够的内存),由于任务不断的向队列中添加,最终结果就是OOM!

(2)案例二,测试代码如下:

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class T15_FullGC_Problem02 {
    private static class CardInfo {
        BigDecimal price = new BigDecimal(0.0);
        String name = "张三";
        int age = 5;
        Date birthdate = new Date();

        public void m() {System.out.println(Thread.currentThread().getName());}
    }

    private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
            new ThreadPoolExecutor.DiscardOldestPolicy());

    public static void main(String[] args) throws Exception {
        executor.setMaximumPoolSize(55);
        executor.setThreadFactory(new UserThreadFactory("T15_FullGC_Problem02-pool"));

        for (;;){
            modelFit();
            Thread.sleep(100);
        }
    }

    private static void modelFit(){
        List<CardInfo> taskList = getAllCardInfo();
        taskList.forEach(info -> {

            // do something
            executor.scheduleWithFixedDelay(() -> {
                //do sth with info
                info.m();

            }, 2, 3, TimeUnit.SECONDS);
        });
    }

    private static List<CardInfo> getAllCardInfo(){
        List<CardInfo> taskList = new ArrayList<>();

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

        return taskList;
    }
}

    class UserThreadFactory implements ThreadFactory {

    //线程组名称
    private final String namePrefix;

    //线程自增序号
    private final AtomicInteger nextId = new AtomicInteger(1);

    // 定义线程组名称,在 jstack 问题排查时,非常有帮助
    public UserThreadFactory(String whatFeaturOfGroup) {
        namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-Worker-"; }

    @Override
    public Thread newThread(Runnable task) {
        String name = namePrefix + nextId.getAndIncrement();
        Thread thread = new Thread(null, task, name, 0);
        return thread;
    }
}

对比案例一,此处我们使用了自定义的线程池名称、线程名称;
javac编译后运行程序,通过arthas直接观察此时的线程情况:
在这里插入图片描述
注意看此时NAME,打印的是我们自定的线程池名称!因此创建线程或线程池时指定有意义的线程名称,方便出错时回溯,迅速定位问题!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值