前置知识补充关于线程池的使用,有问题的同学可以查看上篇博客内容:
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,打印的是我们自定的线程池名称!因此创建线程或线程池时指定有意义的线程名称,方便出错时回溯,迅速定位问题!