JVM调优与问题排查

JVM 调优分类

调优是一个很大的概念,简单说就是把系统进行优化,但是站在系统的角度,我们一般把jvm条又分为三部分:
1.JVM预调优
2.调优JVM 运行环境(慢、卡顿等)
3.解决JVM中的问题(OOM、CPU飙升等)
调优中,现象最明显的是 OOM,因为有异常抛出,当然它也只是作为调优的一部分。预调优和优化运行环境估计很多人做的就是服务器重启而已。前一篇文章调优就是JVM内存结构调优的一种方式。

JVM 预调优

业务场景设定

调优是要分场景的,所以一定要明显你调优项目的场景设定,像现在大家都是微服务架构了,服务拆分出来以后更加适合做场景设定。
比如这个服务就注重吞吐量,这个服务注重用户的体验(用户的响应时间)等等。

无监控不调优

这里的监控指的是压力测试,能够看到结果,有数据体现的,不要用感觉去优化,所有的东西一定要有量化的指标,比如吞吐量,响应时间,服务器资源,网络资源等等。总之一句话,无监控不优化。

处理步骤

计算内存需求

计算内存需求,内存不是越大越好,对于一般系统来说,内存的需求是弹性的,内存小,回收速度快也能承受。所以内存大小没有固定的规范。虚拟机栈的大小在高并发情况下可以变小。
元空间(方法区)保险起见还是设定一个最大的值(默认情况下元空间是没有大小限制的),一般限定几百 M 就够用了,为什么说还限定元空间。
举例子:一台 8G 的内存的服务器,如果运行时还有其他的程序加上虚拟机栈加上元空间,占用超过 6 个 G 的话,那么我们设定堆是弹性的(max=4G),那么其实堆空间拓展也超不过 2G,所以这个时候限制元空间还是有必要的。

选定 CPU

对于系统来说, CPU 的性能是越高越好,这个按照你的预算来定(CPU 的成本很高)。
尤其是现在服务器做了虚拟机化之后,虚拟机的性能指标不能单看虚拟化后的参数指标,更应该看实际物理机的情况。

选择合适的垃圾回收器

对于吞吐量优先的场景,就只有一种选择,就是使用 PS 组合(Parallel Scavenge+Parallel Old )
对于响应时间优先的场景,在 JDK1.8 的话优先 G1,其次是 CMS 垃圾回收器。

设定新生代大小、分代年龄

根据我上一篇文章的测试结果可以看出,吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的新生代和一个较小的老年代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。而设置分代年龄可以使S0、S1区的对象尽快的被YGC回收掉,吞吐量大部分回收的都是朝生夕死的对象。长期存活的对象或大对象就进入老年代了。

设定日志参数

-XX:+PrintGC 输出 GC 日志
-XX:+PrintGCDetails 输出 GC 的详细日志
-XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息
-Xloggc:…/logs/gc.log 日志文件的输出路径
注意:一般记录日志的是,如果只有一个日志文件肯定不行,有时候一个高并发项目一天产生的日志文件就上 T,其实记录日志这个事情,应该是运维干的事情。日志文件帮助我们分析问题。

优化 JVM 运行环境(慢、卡顿等)

一般造成 JVM 卡或者慢的原因无非两个部分,一个是 CPU 占用过高,一个是内存占用过高。所以这个时候需要我们进行问题的排查,进行具体的故障分析。
解决 JVM 中的问题(OOM 等)
常见的内存溢出包括,栈溢出,堆溢出、元空间(方法区)、直接内存溢出。当然内存溢出也可能是内存泄漏导致的。
比较常见的是堆内存OOM,一般是for循环频繁创建对象没有回收掉导致的。

亿级流量电商系统 JVM 调优

亿级流量系统

在这里插入图片描述
亿级流量系统,其实就是每天点击量在亿级的系统,根据淘宝的一个官方的数据分析。每个用户一次浏览点击 20~40 次之间,推测出每日活跃用户(日活用户)在 500 万左右。同时结合淘宝的一个点击数据,可以发现,能够付费的也就是橙色的部分(cart)的用户,比例只有 10%左右。90%的用户仅仅是浏览,那么我们可以通过图片缓存、Redis 缓存等技术,我们可以把 90%的用户解决掉。10%的付费用户,大概算出来是每日成交 50 万单左右。当然还有服务器的负载均衡分流。

GC 预估

如果是普通业务,一般处理时间比较平缓,大概在 3,4 个小时处理,算出来每秒只有几十单,这个一般的应用可以处理过来(不需要 JVM 预估调优)另外电商系统中有大促场景(秒杀、限时抢购等),一般这种业务是几种在几分钟。我们算出来大约每秒 2000 单左右的数据,
假设承受大促场景的使用 4 台服务器(使用负载均衡)。每台订单服务器也就是大概 500 单/秒我们测试发现,每个订单处理过程中会占据 0.2MB 大小的空间(什么订单信息、优惠券、支付信息等等),那么一台服务器每秒产生 100M 的内存空间,这些对象基本上都是朝生夕死,也就是 1 秒后都会变成垃圾对象。

在这里插入图片描述

加入我们设置堆的空间最大值为 3 个 G,我们按照默认情况下的设置,新生代 1/3 的堆空间,老年代 2/3 的堆空间。Eden:S0:S1=8:1:1
我们推测出,old 区=2G,Eden 区=800M,S0=S1=100M
根据对象的分配原则(对象优先在 Eden 区进行分配),由此可得,8 秒左右 Eden 区空间满了。
每 8 秒触发一个 MinorGC(新生代垃圾回收),这次 MinorGC 时,JVM 要 STW,但是这个时候有 100M 的对象是不能回收的(线程暂停,对象需要 1 秒后都会变成垃圾对象),那么就会有 100M 的对象在本次不能被回收(只有下次才能被回收掉)
所以经过本次垃圾回收后。本次存活的 100M 对象会进入 S0 区,但是由于另外一个 JVM 对象分配原则(如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄)所以这样的对象本质上不会进去 Survivor 区,而是进入老年代

在这里插入图片描述
所以我们推算,大概每个 8 秒会有 100M 的对象进入老年代。大概 20*8=160 秒,也就是 2 分 40 秒左右 old 区就会满掉,就会触发一次 FullGC,一般来说,这次 FullGC 是可以避免的,同时由于 FullGC 不单单回收老年代+新生代,还要回收元空间,这些 FullGC 的时间可能会比较长(老年代回收的朝生夕死的对象,使用标记清除/标记整理算法决定了效率并不高,同时元空间也要回收一次,进一步加大 GC 时间)。所以问题的根本就是做到如何避免没有必要的 FullGC.因为秒杀活动的时间很短,需要尽量避免不必要的FullGC。

GC 预估 调优

我们在项目中加入 VM 参数:
-Xms3072M -Xmx3072M -Xmn2048M
-XX:SurvivorRatio=7 -Xss256K -XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128M
-XX:MaxTenuringThreshold=2
-XX:ParallelGCThreads=8
-XX:+UseConcMarkSweepGC
1、首先看一下堆空间:old 区=1G,Eden 区=1.4G,S0=S1=300M
在这里插入图片描述
2、那么第一点,Eden 区大概需要 14 秒才能填满,填满之后,100M 的存活对象会进入 S0 区(由于这个区域变大,不会触发动态年龄判断)
在这里插入图片描述
3、再过 14 秒,Eden 区,填满之后,还是剩余 100M 的对象要进入 S1 区。但是由于原来的 100M 已经是垃圾了(过了 14 秒了),所以,S1 也只会有 Eden 区过来的 100M 对象,S0 的 100M 已经别回收,也不会触发动态年龄判断。
4、反反复复,这样就没有对象会进入 old 区,就不会触发 FullGC,同时我们的 MinorGC 的频次也由之前的 8 秒变为 14 秒,虽然空间加大,但是换来的还是 GC 的总时间会减少。
5、-Xss256K -XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128M 栈一般情况下很少用到 1M(默认1M)。所以为了线程占用内存更少,我们可以减少到 256K ,元空间一般启动后就不会有太多的变化,我们可以设定为 128M,节约内存空间。
6、-XX:MaxTenuringThreshold=2 这个是分代年龄(年龄为 2 就可以进入老年代),因为我们基本上都使用的是 Spring 架构,Spring 中很多的 bean 是长期要存活的,没有必要在 Survivor 区过渡太久,所以可以设定为 2,让大部分的 Spring 的内部的一些对象进入老年代。
7、-XX:ParallelGCThreads=8 线程数可以根据你的服务器资源情况来设定(要速度快的话可以设置大点,根据 CPU 的情况来定,一般设置成 CPU 的整数倍默认是2)

在这里插入图片描述
8、-XX:+UseConcMarkSweepGC 因为这个业务响应时间优先的,所以还是可以使用 CMS 垃圾回收器或者 G1 垃圾回收器。当然并发垃圾回收期并CPU要求比较高。
在这里插入图片描述

模仿线上的调优案例

在这里插入图片描述

package ex13;

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

/**
 * VM参数: -XX:+PrintGC -Xms200M -Xmx200M
 *  GC调优---生产服务器推荐开启(默认是关闭的)
 *  -XX:+HeapDumpOnOutOfMemoryError
 */

public class FullGCProblem {
    //线程池
    private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
            new ThreadPoolExecutor.DiscardOldestPolicy());

    public static void main(String[] args) throws Exception {
        //50个线程
        executor.setMaximumPoolSize(50);
        while (true){
            calc();
            Thread.sleep(100);
        }
    }
    //多线程执行任务计算
    private static void calc(){
        List<UserInfo> taskList = getAllCardInfo();
        taskList.forEach(userInfo -> {
            executor.scheduleWithFixedDelay(() -> {
                userInfo.user();
            }, 2, 3, TimeUnit.SECONDS);
        });
    }
    //模拟从数据库读取数据,返回
    private static List<UserInfo> getAllCardInfo(){
        List<UserInfo> taskList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            UserInfo userInfo = new UserInfo();
            taskList.add(userInfo);
        }
        return taskList;
    }
    private static class UserInfo {
        //假设获取用户信息
        String name = "kingdom";
        int age = 18;
        BigDecimal money = new BigDecimal(999999.99);
        public void user() {
            //
        }
    }
}

在 Linux 服务跑起来
java -cp xxxx.jar -XX:+PrintGC -Xms200M -Xmx200M ex13.FullGCProblem

CPU 占用过高排查

  1. 先通过 top 命令找到消耗 cpu 很高的进程 id 假设是 2738
    top 命令是我们在 Linux 下最常用的命令之一,它可以实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息。其中上半部分显示的是系统的统计信息,下半部分显示的是进程的使用率统计信息。
    在这里插入图片描述
    在这里插入图片描述2. 执行 top -p 2738 单独监控该进程
    3 、在第 2 步的监控界面输入 H ,获取当前进程下的所有线程信息
    在这里插入图片描述
    4 、找到消耗 cpu 特别高的线程编号,假设是 2740、2741 (要等待一阵)
    5 、执行 jstack 2738 对当前的进程做 dump
    在这里插入图片描述
    6.然后把pid对应的 2740、2741 转换成16进制可以算出
    在这里插入图片描述
    在这里插入图片描述
    7 根据第 6 步得到的 0xab4 在第 5 步的线程信息里面去找对应线程内容,对应是2条GC线程
    8 解读线程信息,定位具体代码位置
    发现找是 VM 的线程占用过高,我们发现我开启的参数中,有垃圾回收的日志显示,所以我们要换一个思路,可能是我们的业务线程没问题,而是垃圾回收的导致的。

Jstat (代码中有打印 GC 参数,生产上可以使用这个 jstat –gc 来统计,达到类似的效果)
是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
假设需要每 2500 毫秒查询一次进程 13616 垃圾收集状况,一共查询 10 次,那命令应当是:jstat -gc 2738 2500 10
在这里插入图片描述
在这里插入图片描述
jstat -gc 2738 5000 20 | awk ‘{print $13,$14,$15,$16,$17 }’
在这里插入图片描述
使用这个大量的 FullGC 了还抛出了 OUT Of Memory,基本上一直在FullGC了。
我们可以看到,这个里面 CPU 占用过高是什么导致的?
是业务线程吗?不是的,这个是 GC 线程占用过高导致的。JVM 在疯狂的进行垃圾回收,JVM 中默认的垃圾回收器是多线程的,所以多线程在疯狂回收,导致 CPU 占用过高。

内存占用过高调优思路

用于生成堆转储快照(一般称为 heapdump 或 dump 文件)。jmap 的作用并不仅仅是为了获取 dump 文件,它还可以查询 finalize 执行队列、Java 堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。和 jinfo 命令一样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的-dump 选项和用于查看每个类的实例、空间占用统计的**-histo** 选项在所有操作系统都提供之外
把 JVM 中的对象全部打印出来, 但是这样太多了,那么我们选择前 20 的对象展示出来,
jmap –histo 2738 | head -20
在这里插入图片描述
在这里插入图片描述

问题总结

一般来说,前面这几行,就可以看出,到底是哪些对象占用了内存。
这些对象回收不掉吗?是的,这些对象回收不掉,这些对象回收不掉,导致了 FullGC,里面还有 OutOfMemory。
任务数多于线程数,那么任务会进入阻塞队列,就是一个队列,你进去,排队,有机会了,你就上来跑。但是,因为代码中任务数一直多于线程数,所以每 0.1S,就会有 50 个任务进入阻塞对象,50 个任务底下有对象,至少对象送进去了,但是没执行。所以导致对象一直都在,同时还回收不了。
为什么回收不了。Executor 是一个 GCroots
在这里插入图片描述
所以堆中,就会有对象 60 万个,阻塞队列中 60 万个任务,futureTask。并且这些对象还回收不了。

总结

在 JVM 出现性能问题的时候。(表现上是 CPU100%,内存一直占用)
1、 如果 CPU 的 100%,要从两个角度出发,一个有可能是业务线程疯狂运行,比如说想很多死循环。还有一种可能性,就是 GC 线程在疯狂的回收,因为 JVM 中垃圾回收器主流也是多线程的,所以很容易导致 CPU 的 100%
2、 在遇到内存溢出的问题的时候,一般情况下我们要查看系统中哪些对象占用得比较多,我的是一个很简单的代码,在实际的业务代码中,找到对应的对象,分析对应的类,找到为什么这些对象不能回收的原因,就是可达性分析算法,JVM 的内存区域,还有垃圾回收器的基础,当然,如果遇到更加复杂的情况,你要掌握的理论基础远远不止这些(JVM 很多理论都是排查问题的关键)。

常见问题分析
超大对象
代码中创建了很多大对象 , 且一直因为被引用不能被回收,这些大对象会进入老年代,导致内存一直被占用,很容易引发 GC 甚至是 OOM

超过预期访问量
通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。
比如如果一个系统高峰期的内存需求需要 2 个 G 的堆空间,但是堆空间设置比较小,导致内存不够,导致 JVM 发起频繁的 GC 甚至OOM

过多使用 Finalizer
过度使用终结器(Finalizer),对象没有立即被 GC,Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的 CPU 时间较少,因此它永远也赶不上主线程的步伐,程序消耗了所有的可用资源,最后抛出 OutOfMemoryError 异常。

内存泄漏
1.大量对象引用没有释放,JVM 无法对其自动回收。
2.长生命周期的对象持有短生命周期对象的引用
例如将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
3.连接未关闭
如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值