数据倾斜导致的频繁FullGC问题排查

问题发现 线上兼容系统报警,提示有频繁的FullGC以及GC耗时问题比较严重。 问题定位 在收到FullGC报警之后,登录到内部的监控系统,看一下集群整体的GC情况(如果没有这样的监控系统,可以去机器上查看GC日志): 从图中可以看到,从大概9:03分开始,对内存的老年代就一直在涨,并且在9:13的时候发生了很多次fullGC。 这时候,第一时间去把堆dump下来了,我先在GC前做了一次Dump,然后再做了一次GC,然后做了一次Dump,发现GC前的Dump中很多String占用了比较多的内存,但是GC后就都被回收了。 然后去看这些字符串都是啥,但是很遗憾,没看出什么特别有价值的东西,都是一些游离的字符串(一方面通过堆dump发现他们不可达,另外发现FullGC后就直接回收了) 这时候就在分析,为啥会发生这样的情况。于是我怀疑,可能是因为在9:00-9:20这段时间,请求量比较大,创建了很多对象分配到年轻代,但是年轻代不够了,就被分配到老年代。然后老年代一直在GC,但是因为同时又有大量的操作导致对象也在不断往老年代去。所以看上去老年代在9:13分左右的GC效果并不明显。 为了验证这个猜想,同时看了下年轻代的情况,Eden区没啥明显变化,但是Survivor区的增长比较大: 于是感觉这个方向是靠谱的,大概率是这段时间的请求量太大了,导致很多对象被分配到老年代,然后触发了很多次FullGC。 于是开始看系统监控,找这段时间有没有哪些接口或者请求的量比较大。 1、先是翻了我们的RPC接口的调用量监控,发现并无异常。 2、继续翻了MQ的消费量监控,发现并无异常。 3、查看对外部接口的调用量监控,发现有一个接口的调用量QPS比较高。 4、查看TDDL的QPS(TDDL是阿里内部的数据库访问中间件,可以简单认为是数据库连接池)飙高明显。 结合这两个现象,一个是具体哪个接口调用量变大了,一个是哪张表的写操作变多了。而且时间也刚好对得上,于是就继续分析。 开始找这部分代码的调用链,发现是一个定时任务,扫表进行数据操作的。而且时间也对得上,每小时执行一次,基本上是在整点开始执行,大概15分钟执行完。 这里交代一下背景,我们的定时任务是分布式任务,也就是说比如有10000条数据,会有10台机器并发去扫描这些数据来处理。 这里奇怪的是只有一台机器有这个FullGC以及堆被干满的情况,这不应该啊。于是我开始逐台机器查看他最近12小时的堆内存情况。 还真让我发现了一个规律,那就是在不同的时间点,都会有其中某台机器的内存升高,有的时候会有FullGC,有的时候没有,而且基本都是在整点左右开始,并且也伴随着前面发现的TDDL和接口的调用量飙升。 以下是另外一台机器在前一天19点左右的堆内存及GC情况。 再来总结一下现象: 1、每个整点开始,会有一个扫表任务开始执行,任务是分布式,多台机器并发开始执行。 2、任务开始执行后,会有某台机器的老年代内存被干满,导致频繁的FullGC 3、GC是有效果的,说明这些对象都是垃圾对象。可以被回收的。那就是因为年轻代放不下了导致的,而不是内存泄漏。 到这里,聪明的我马上想到了问题可能出在哪了。(机智脸 然后我开始验证我的想法,找到一个整点时间点,然后去看这个整点之后的15分钟内,有堆内催增长的机器,和没有堆内存增长(或者增长不明显)的机器,对比日志。 假设:9:00是11.11.11.11这台机器FullGC了, 10:00 是22.22.22.22这台机器FullGC了。 那么就这么对比: 11.11.11.11机器在9:00 - 10:00之间的日志,和,11.11.11.11机器在8:00-9:00之间的日志对比。 22.22.22.22机器在9:00 - 10:00之间的日志,和,22.22.22.22机器在10:00-11:00之间的日志对比。 然后我就发现了问题: 11.11.11.11机器在9:00 - 10:00之间和22.22.22.22机器在10:00-11:00之间扫描的数据范围是一样的,都是用户ID以22作为开头的数据。 因为我们是分布式任务扫表,为了扫表不重复,所以会根据用户ID的前两位进行分段,然后随机给不同的机器去扫描不同的前缀的用户。 刚好22开头的用户太多(数据严重倾斜)导致分到这段的机器就要处理大量数据,处理过程中会创建很多对象,导致内存被占用。然后因为随机分的,所以不同的时段会有不同的机器内存被干满。 可以看到22开头的明显比别的多很多: 如下图,是一台6:00-7:00之间有FullGC的机器的日志情况: 问题解决 问题定位到了,解决就容易了,想办法让倾斜的数据在分布式任务扫表的时候均分就行了。有几个办法: 1、之前是按照用户ID前两位分的,那么就再分的细一点,按照前3位分一下。 2、不再按照用户ID分,而是按照主键ID进行分段。(最开始没用这个方案是因为待扫描数据并不连续,区间长度不太好掌握,还有个重要原因就是需要在SQL中针对相同用户做数据聚合。)

March 22, 2026 · 1 min · santu

数据库CPU被打满排查过程

问题发现 最近,经常收到一些数据库的报警,提示我们的数据库的CPU有异常飙高的情况,通过该监控发现,确实间歇性的有一些CPU飙高的情况,经常把CPU打满了。 问题排查 通过监控进一步查看,发现在CPU飙高的同时,有大量SQL的锁耗时比较长,平均在1.5秒左右,并且在业务高峰期经常要4s-5s: 具体查看SQL的话,会发现是一些update语句导致的: 主要的SQL内容在下面,其中我们的更新条件,number是有唯一性索引的: 1 2 SET gmt_modified = now(), business_type_enum = ?, product_type_enum = ? WHERE number = ? 经过分析SQL语句,结合前面我们看到的这条SQL语句大量的耗时都在锁等待上面。现象比较明显了,那问题根据猜测,大概率是出现在多个线程同时尝试更新同一行记录的时候。 因为InnoDB会在update的时候自动给行记录加锁,以防止其他线程同时更新该行记录。如果多个线程同时尝试更新同一行记录,那么没拿到锁的线程就必须等待持有锁的线程释放锁后才能继续更新该行记录。 ✅MySQL热点数据更新会带来哪些问题? 思路大致有了,再结合我们的实际业务情况,基本可以确定之所以导致CPU飙高就是因为并发修改同一条记录导致的锁等待,进而导致的CPU飙高。 因为CPU飙高的几个时间点,都是我们有一个合案任务在执行,合案任务的逻辑是这样的: 定时扫描所有风控策略流入的存在欺诈风险的订单及用户数据(fraud_risk_order),然后把这些数据,基于用户维度进行合并,并且合并后要把多条其他明细数据组合在同一条用于审核的欺诈审核单(fraud_audit_order)上。 伪代码: 1 2 3 4 for(fraud_risk_order : fraud_risk_orders){ update fraud_audit_order set xxx = 'xx' where fraud_audit_order_no = "同一个单号" } 而这个问题之前没有出现,是在我最近刚刚做过一次合案任务的性能优化,使用网格任务分布式的进行合案之后才频繁出现的。 主要就是任务的性能好了,扫表扫的快了,如果同一个用户名下的欺诈风险单比较多的话,就会并发的去修改同一条审核单。这就会导致并发冲突。 问题解决 问题定位到以后,就要想办法解决了。 结合我们自己的业务情况,优化的方案也很简单,就是不要单条单条的去修改审核单,而是先进行一次预合案,然后再批量一次执行更新,并把结果合并到一条审核单上即可。 预合案的方案主要是基于数据库写SQL做的,大致思路如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 select product_type_enum, subject_id, subject_id_enum, GROUP_CONCAT(distinct(submitter) SEPARATOR ',') as submitters, GROUP_CONCAT(distinct(number) SEPARATOR ',') as risk_order_numbers, GROUP_CONCAT(distinct(risk_level_enum) SEPARATOR ',') as risk_level_enums , GROUP_CONCAT(distinct(risk_category) SEPARATOR ',') as category_codes from fraud_risk_order where product_type_enum = "XXX" and risk_order_status_enum = 'DRAFT' group by subject_id_enum,subject_id 通过上面的SQL,我们把各个需要合并的数据,基于主体ID和主体类型进行了聚合,并且把需要聚合到一起的字段,如submitter,通过GROUP_CONCAT函数进行逗号分隔开组成一个字符串。 ...

March 22, 2026 · 1 min · santu

数据库死锁问题排查过程

(这个问题我排查了很久,期间也找很多DBA帮忙排查过,但是最终还是靠自己断断续续排查了一个月的时间才定位到,问题还是挺有意思的,大家可以看看。如果看不懂我觉得也没关系,因为他牵扯到的内容比较多,需要一些背景知识,后面再回过头来看就好了) 现象 某天晚上,同事正在发布,突然线上大量报警,很多是关于数据库死锁的,报警提示信息如下: 1 2 3 4 5 6 7 8 9 {"errorCode":"SYSTEM_ERROR","errorMsg":"nested exception is org.apache.ibatis.exceptions.PersistenceException: Error updating database. Cause: ERR-CODE: [TDDL-4614][ERR_EXECUTE_ON_MYSQL] Deadlock found when trying to get lock; The error occurred while setting parameters\n### SQL: update fund_transfer_stream set gmt_modified=now(),state = ? where fund_transfer_order_no = ? and seller_id = ? and state = 'NEW' 通过报警,我们基本可以定位到发生死锁的数据库以及数据库表。先来介绍下本文案例中涉及到的数据库相关信息。 背景情况 我们使用的数据库是Mysql 5.7,引擎是InnoDB,事务隔离级别是READ-COMMITED。 数据库版本查询方法: 1 SELECT version(); 引擎查询方法: 1 show create table fund_transfer_stream; 建表语句中会显示存储引擎信息,形如:ENGINE=InnoDB ...

March 22, 2026 · 5 min · santu

数据库连接池满排查过程

问题发现 之前一段时间,业务在线上经常出现频繁的数据库连接池满的报警,报错信息如下: 1 2 3 4 Caused by: ERR-CODE: [TDDL-4103][ERR_ATOM_CONNECTION_POOL_FULL] Pool of DB 'cn-zhxxx_i-xxx_fin_risk_xxx_30xx:33.10.xxx.xx:30xx' is full. Message from pool: wait millis 5000, active 10, maxActive 10. AppName:FIN_RISK_xxx_APP, Env:ONLINE, UnitName:null. 问题排查 数据库连接池满了,有很多种情况,比如线程数不够了,并发请求太高了,有慢SQL等等。因为这个业务一直在线上跑着,先去看了看流量监控,发现并没什么特别的异常。 于是怀疑是慢SQL,然后去排查了一下SQL耗时,发现有大量的耗时SQL,并且执行耗时和锁耗时差不多是相等的。(公司内部工具) 然后看下这条具体的SQL语句: 1 2 3 UPDATE collection_case SET gmt_modified = now(), lock_version = lock_version + ?, hands_count = ?, case_state = ?, max_ovd_days = ?, case_class = ?, cur_ovd_principal = ?,collection_amount = ? WHERE id = ? AND deleted = ? AND lock_version = ? 其实就是简单的一个更新语句,其中使用了乐观锁进行并发控制。 ...

March 22, 2026 · 1 min · santu

日志打印导致CPU飙高问题排查

(本案例来自我的数藏项目中压测相关问题,更多类似问题和具体过程,在项目课中均有讲解) 问题出现 在我们的压测过程中,我们出现过一次因为日志打印占用CPU过的问题,当时的情况是这样的。 4C8G的机器,单商品设置5000库存,采用秒杀第二套方案Redis+MQ+Mysql的方案进行扣减,QPS在100进行压测。(详见压测视频) 压测过程中出现CPU飙高。 通过top命令查看CPU在300%以上,因为我们是4核的机器,所以基本上CPU利用率在80%以上了。 通过prometheus的监控也能看到CPU飙高的比较明显。 问题定位 紧接着通过arthas进行问题排查。 先通过github下载arthas: https://github.com/alibaba/arthas/releases 下载后解压,然后执行java -jar arthas-boot.jar 进入arthas之后,执行thread -n 3 查看占用CPU最高的三个线程,可以看到第一个线程的情况是: 通过以上信息我们可以知道,这个线程占用CPU在50%左右,并且通过多次执行thread命令后发现他都是持续排在第一的。那说明他一定有问题。 于是通过堆栈信息,发现这是一个和日志打印有关的代码。因为可以看到堆栈的最后一行,提示这个是logback的AsyncAppender的调用。 这时候基本定位到和日志打印有关。那我们看下我们的logback的配置,找到代码中的logback-spring.xml ,找到AsyncAppender的配置内容如下: 以上配置问题很大,我们看下这几个配置的作用分别是什么。 属性 默认值 说明 queueSize 256 队列容量(日志事件数)。队列满时根据策略处理新事件。 discardingThreshold 20 队列剩余容量阈值(百分比)。低于阈值时丢弃INFO以下级别日志(TRACE/DEBUG)。 neverBlock false 队列满时:true=直接丢弃新事件false=阻塞等待队列空间(可能影响性能)。 includeCallerData false true=异步获取调用者信息(类/方法名),有性能损耗,必要时启用。 maxFlushTime 1000 关闭时等待队列处理完成的超时时间(毫秒),超时后丢弃剩余事件。 通过了解这些参数的配置之后,我们可以发现,我们的配置有几个问题: 1、queueSize 太小 队列小的话就容易满,满了就可能到导致阻塞 2、discardingThreshold配置为0 为0的时候表示所有级别日志都会进入队列,并且不会因队列剩余容量触发自动丢弃 3、没有配置neverBlock false默认的,则表示队列满的时候,会阻塞等待队列空间,这会大大影响性能 问题解决 所以,我们做如了下修改: 另外,我们也通过日志查看,检查了一下下单接口的日志,发现应用中有大量的和shardingjdbc的SQL有关的日志,其实这些日志可以不用打印的。所以我们修改配置项, 让这个SQL不打印: 修改之后,重新进行压测,则CPU下降明显, 基本维持在50%以下,通过arthas查看线程占用情况,这个日志的线程也基本上占用cpu在10%左右。当然还有优化空间,但是优化肯定是有必要再做,不要做过度优化。 这个改造之后,Redis的扣减和数据库的扣减可以控制在2s以内的延迟了,我们认为可以接受了。如果后继有更高的QPS了,那么如果又出现瓶颈,则我们再调整即可。 ...

March 22, 2026 · 1 min · santu

服务器被注入挖矿木马问题排查

我有一台云服务器,用于部署一个我的个人博客的。最近收到告警,说挂了挖矿程序 首先看下这个程序是不是已经在跑了,top看下运行的进程,发现有一个kswapd0进行耗CPU比较高,这个进程本身是个linux的后台进程,负责内存交换的,但是熟悉的都知道,他其实是个老演员了。 正常的kswapd0不会一直在,也不会占CPU这么高,所以怀疑接下来用 netstat -antlp 看看网络连接情况,挖矿不可能没有远程传输吧。 不查不知道,一查就发现了老朋友。通过netstat发现一个瑞士的IP在和kswapd0通信,同时有两个一个罗马尼亚的ip在和rsync这个数据传输进程在不断通信。 接下来就要深挖了,找到这个kswapd0占用的文件路径到底在哪,进入/proc/这个目录,我这里的是67498: 最终发现是在git用户在有个文件夹,那这个文件夹就可以直接干掉了。 接下来就是删除文件夹、kill掉进程 检查并清除定时任务,删除被黑的账号, 清楚这个用户的密钥: 然后机器就恢复正常了。

March 22, 2026 · 1 min · santu

死循环会导致CPU使用率升高吗?为什么?

典型回答 死循环会导致CPU使用率升高。当代码中出现死循环时,涉及的线程会不断执行循环中的指令而不会退出,因此会持续占用处理器资源。这种情况下,CPU会花费所有可用的时间片去执行这个无尽的循环,导致几乎没有资源剩余来处理其他任务或线程。 Talk Is Cheap,Show Me The Code!!! 我们可以通过实验验证上面的结论,以下实验在8C16G的物理机上实现。内存total=15g,14g可用,因为有1g是因为Linux内核分配给SLAB了。 编写死循环代码: 1 2 3 4 5 6 7 public class Main { public static void main(String[] args) { while (true){ System.out.println(System.currentTimeMillis());//死循环输出系统时间戳,强制产生2个系统调用,可以看到CPU us 和sy 占比都会升高 } } } 没有跑以上代码之前的CPU使用率下图所示,us代表用户态1%,sy代表内核态0.5%。 运行代码后,CPU使用率 us和sy都有升高,同时进程列表中的java进程CPU使用一列也大幅度升高。 注意:俩个java进程,一个是IDEA编译器的,一个是通过IDEA启动的测试代码,由于死循环疯狂在IDEA控制台输出系统时间戳,导致IDEA的CPU使用率也升高了。 如果把死循环输出的那一行注释掉,可以看到只有us用户态的使用率升高了,同时IDEA的进程使用率不会升高。原因是System.out.println()和System.currentTimeMillis()都会产生系统调用进入内核态,注释掉代码后,while死循环只是用户态的程序代码,不需要进入内核态了。

March 22, 2026 · 1 min · santu

死锁会导致CPU使用率升高吗?为什么?

典型回答 死锁不会导致升高,甚至可能降低。因为死锁发生时,涉及的线程会在等待获取锁时被挂起,而不是处于忙碌等待状态。因此,这些线程不会占用CPU资源进行计算,但它们会保持在等待状态,直到死锁被解决。 Talk Is Cheap,Show Me The Code!!! 我们可以通过实验验证上面的结论,以下实验在8C16G的物理机上实现。内存total=15g,14g可用,因为有1g是因为Linux内核分配给SLAB了。 编写死锁代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class Main { public static void main(String[] args) { int threadCnt = 2000; //多创建一些线程观察CPU使用率更合理 Object[] locks = new Object[threadCnt]; for (int i = 0; i < threadCnt; i++) { //初始化线程锁对象 locks[i] = new Object(); } for (int i = 0; i < threadCnt; i++) { //为了产生死锁,我们需要一个线程抢占2把锁,以下代码控制创建时相邻2个线程分别拿到相同的锁 if (i % 2 == 0) { //确保前一个线程拿锁顺序:锁1->锁2 new Thread(new DeadLockTest(locks[i], locks[i + 1])).start(); } else { //确保下一个线程拿锁顺序:锁2->锁1 new Thread(new DeadLockTest(locks[i], locks[i - 1])).start(); } } } public static class DeadLockTest implements Runnable { private final Object lock1; private final Object lock2; public DeadLockTest(Object lock1, Object lock2) { this.lock1 = lock1; this.lock2 = lock2; } @Override public void run() { synchronized (lock1) { System.out.println(Thread.currentThread().getName() + " get Lock:" + lock1); //打印线程名抢到的锁 try { Thread.sleep(1);//休眠1毫秒保证别的线程有机会拿到下面依赖的锁 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println(Thread.currentThread().getName() + " wait Lock:" + lock2);//打印线程名要拿的锁 synchronized (lock2) {//以下会由于拿锁顺序不正确,产生死锁 System.out.println(Thread.currentThread().getName() + " get Lock:" + lock2);//这里由于死锁不会打印出来 } } } } } 为了验证死锁线程会不会导致CPU使用率升高,我们需要弄许多线程死锁,这样才能模拟出线上环境业务线程多,并且因为业务代码错误导致的死锁,来观察对CPU的影响。 运行前的CPU使用率 ...

March 22, 2026 · 2 min · santu

频繁FullGC问题排查

问题发现 通过监控工平台,查看到定价集群存在少量FullGC的情况,FullGC对于程序员来说其实是不能忍的,于是开始排查。 监控提示是MetaSpace发生fullGC,Metaspace中的类需要满足什么条件才能够被当成垃圾被卸载回收? 条件还是比较严苛的,需同时满足如下三个条件的类才会被卸载: 该类所有的实例都已经被回收; 加载该类的ClassLoader已经被回收; 该类对应的java.lang.Class对象没有任何地方被引用。 问题定位 初步推测是有不停的动态创建类的过程,且有类未被回收;后来考虑到定价中存在轻量的表达式引擎AviatorEvaluator,会存在此过程; 于是dump出metaspace和heap 可以看到,存在非常多的Scrip_${timestamp}_${idx}类型的类 重复加载的类中,Top都是关于lambda表达式的 且指向了com.googlecode.aviator包 - 表达式引擎 找到了我的代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 /** * 表达式处理工具类 * * @author Hollis */ public class ExpressionUtil { public static AviatorEvaluatorInstance aviatorEvaluator = AviatorEvaluator.getInstance(); static { aviatorEvaluator.setOption(Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL, true); aviatorEvaluator.setOption(Options.ALWAYS_PARSE_INTEGRAL_NUMBER_INTO_DECIMAL, true); } public static boolean verify(String expression, Map<String, Object> params) { return (Boolean)aviatorEvaluator.compile(expression).execute(params); } /** * 表达式计算 * @param expression 表达式 * @param params 需要替换的表达式参数 * @return calculate result */ public static BigDecimal calculate(String expression, Map<String, Object> params) { BigDecimal result = (BigDecimal)aviatorEvaluator.compile(expression).execute(params); return result.setScale(6, RoundingMode.HALF_UP); } } /** * Compile a text expression to Expression Object without caching * * @param expression * @return */ public Expression compile(final String expression) { return compile(expression, false); } 可以看到,我们在使用表达式引擎时,默认是无缓存的模式进行编译的。 AviatorEvaluatorInstance实例初始化时,会生成一个AviatorClassLoader类加载器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final class AviatorEvaluatorInstance { private volatile AviatorClassLoader aviatorClassLoader = initAviatorClassLoader(); private AviatorClassLoader initAviatorClassLoader() { return AccessController.doPrivileged(new PrivilegedAction<AviatorClassLoader>() { @Override public AviatorClassLoader run() { return new AviatorClassLoader(AviatorEvaluatorInstance.class.getClassLoader()); } }); } ... } 编译过程主要分为如下几步 文法分析 初始化编码生成器 语法解析器生成 语法解析,实例化Class,最终的 Expression 对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * 编译过程 **/ private Expression innerCompile(final String expression, final String sourceFile, final boolean cached) { // 文法分析 ExpressionLexer lexer = new ExpressionLexer(this, expression); // 初始化编码生成器 CodeGenerator codeGenerator = newCodeGenerator(sourceFile, cached); // 语法解析器生成 ExpressionParser parser = new ExpressionParser(this, lexer, codeGenerator); // 语法解析,实例化Class,最终的 Expression 对象 Expression exp = parser.parse(); if (getOptionValue(Options.TRACE_EVAL).bool) { ((BaseExpression) exp).setExpression(expression); } return exp; } 直接看编码生成器的初始化阶段 首先,获取AviatorClassLoader类型的类加载器 初始化OptimizeCodeGenerator的编码生成器 类型为ASMCodeGenerator classLoader=AviatorClassLoader className=“Script_” + System.currentTimeMillis() + “_” + CLASS_COUNTER.getAndIncrement(); - 与metaspace区的类信息吻合 ...

March 22, 2026 · 4 min · santu

频繁FullGC问题排查(2)

问题发现 线上兼容系统报警,提示有频繁的FullGC以及GC耗时问题比较严重。 问题定位 在收到FullGC报警之后,登录到内部的监控系统,看一下集群整体的GC情况(如果没有这样的监控系统,可以去机器上查看GC日志): 可以看到,集群的GC次数是3小时内有十几次了,那么去看一下单机的情况。 这里我先去看了一下报警的那台机器,然后有随机挑了几台线上机器去查看,看到一个比较特殊的现象: 那就是并不是所有机器都存在FullGC的情况,有些机器的堆内存的水位甚至还挺低的。但是这里没多想,接着去看堆dump去了。 我们因为内部有进行堆dump以及分析的工具,如果么有的话可以使用jmap或者arthas获取堆dump。然后再使用Java VisualVM、Memory Analyzer Tool等工具进行分析。 我这里是分别对存在FullGC的机器、当前堆内存占用比较高的机器、以及内存占用并不高也没有频繁GC的机器进行了dump。 之所有多次dump,主要是为了作对比。 在分析过程中,发现堆内存占用比较高的机器和FullGC比较频繁的机器中,存在着一些比较特殊的现象。 首先是有大对象占了2个多G的内存。 然后再进一步查看大对象内容: 发现有一个ArrayList中存放了60多万个CollectionCaseDO对象。 这个CollectionCaseDO对象我就比较熟悉了,是我们自己的业务模型。但是竟然会在内存中加载这么多就很奇怪了。 看到这里我有两个猜测: 1、在一个bean中有一个List的成员变量,在代码中会多次向其中add,导致他有这么大的量。 2、在代码中有一个地方在做查询的时候没有做好条件过滤及分页,导致数据库查询了大量数据。 第一个猜测很快被我排除了,因为我去全局搜索了代码,并没有发现这种用法。那么就只剩第二种了,接下来排查在哪里出现的这个问题查询。 然后我又想到,线上不是所有机器都有这个现象,只有部分机器,并且通过监控发现,出问题的机器堆内存是逐步增长起来的: 于是,根据问题发生的时间点,去查日志。 在查日志的之前,我根据上面的情况,以及dump的信息,进一步定位到这个问题应该和我们的一个查询接口有关。 于是通过这个接口的关键日志进行查询,还真的让我查到了端倪。 在内存两次增长的时间点,刚好有两条特殊的日志。 正常的查询,参数中是要带一个查询的id或者当前的坐席的,如: 但是上面的问题查询没有带这个ID,那么看了一下代码,这是一个根据ID查询详情的接口,但是发现同事的代码中并没有对这个caseId做非空校验,然后在用户未传递caseId的时候,用了个queryList,就会把所有的案件都查出来放到List中。。。。 截止到这里,后端的问题基本上定位到了,因为没有传ID,并没有做校验,导致一次查询把所有数据都查出来,放到了List中,然后导致大对象被放到老年代占用了大量空间,因为有多次查询,导致FullGC多次。 后面为啥没传caseId就让前端检查了一下,发现是前端的bug,但是也确实是后端没做好校验导致的。 问题解决 问题定位了,解决很简单了,就对caseID做一下非空校验就行了。如果发现没传,直接报错返回即可。

March 22, 2026 · 1 min · santu

留言给博主