mybatis查询条件in导致的OOM之前因后果

这两天看到一篇文章描述了由mybatis导致的oom,感觉有点懵了,这里还有坑?看的我是那个茅塞顿开啊,感觉自己下次换工作,好像又可以吹下自己实际工作中遇见的mybatis坑了,不过为了到时候描述的更逼真一下,还是自己干一下才行!
按照文章介绍,简单搭建一下环境,开始测试!

数据库连接

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/sys?characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&useSSL=false
    username: root
    password: root

Mapper文件

public interface UserMapper extends BaseMapper<User> {

    Long selectCnt(@Param("ids") List<Long> ids);
}

xml文件

    <select id="selectCnt" resultType="java.lang.Long">
        select count(*) from user
where id in <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
    </select>

jvm 堆内存设置(本地很关键,堆内存大了不好演示,调小一点)

-Xmx256m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

开启测试

这里主要是测试in条件在多线程环境下可能会出现oom的情况,sql很简单,就一个count(*)查询。

@Test
   public void test2(){

       AtomicInteger ai = new AtomicInteger(0);

       for(int threadId =0;threadId <50;threadId++){

           int finalThreadId = threadId;
           new Thread(()->{
               List<Long> ids = getIds(100000 + finalThreadId);
               Long aLong = userMapper.selectCnt(ids);
               ai.getAndIncrement();
               System.out.println(finalThreadId +"::开始了");
           }).start();

       }

       while (ai.get() <50){

       }
       System.out.println("结束。。。。。。。。。。");
   }

   private List<Long> getIds(int size){
       List<Long> list =new ArrayList<>();
       for(long i = 0;i<size;i++){
           list.add(i);
       }

       return list;

   }

控制台开始疯狂打印Full GC

[Full GC (Ergonomics) [PSYoungGen: 29696K->29394K(58368K)] [ParOldGen: 174877K->174877K(175104K)] 204573K->204271K(233472K), [Metaspace: 38486K->38486K(1085440K)], 0.0941873 secs] [Times: user=0.64 sys=0.00, real=0.09 secs] 
[Full GC (Ergonomics) [PSYoungGen: 29696K->29407K(58368K)] [ParOldGen: 174877K->174877K(175104K)] 204573K->204284K(233472K), [Metaspace: 38486K->38486K(1085440K)], 0.0937098 secs] [Times: user=0.78 sys=0.00, real=0.09 secs] 
[Full GC (Ergonomics) [PSYoungGen: 29696K->29417K(58368K)] [ParOldGen: 174877K->174877K(175104K)] 204573K->204294K(233472K), [Metaspace: 38486K->38486K(1085440K)], 0.0953067 secs] [Times: user=0.66 sys=0.00, real=0.10 secs] 
[Full GC (Ergonomics) [PSYoungGen: 29696K->28788K(58368K)] [ParOldGen: 174877K->174797K(175104K)] 204573K->203585K(233472K), [Metaspace: 38486K->38486K(1085440K)], 0.1356401 secs] [Times: user=1.19 sys=0.00, real=0.13 secs] 
Exception in thread "Thread-6" java.lang.OutOfMemoryError: Java heap space
[Full GC (Ergonomics) [PSYoungGen: 29696K->29219K(58368K)] [ParOldGen: 172152K->172152K(175104K)] 201848K->201372K(233472K), [Metaspace: 38536K->38536K(1085440K)], 0.0916355 secs] [Times: user=0.75 sys=0.00, real=0.09 secs] 
[Full GC (Ergonomics) [PSYoungGen: 29696K->29236K(58368K)] [ParOldGen: 172152K->172152K(175104K)] 201848K->201389K(233472K), [Metaspace: 38536K->38536K(1085440K)], 0.0861813 secs] [Times: user=0.78 sys=0.00, real=0.09 secs] 
Exception in thread "Thread-35" Exception in thread "Thread-16" java.lang.OutOfMemoryError: GC overhead limit exceeded

Dumping heap to java_pid463104.hprof ...
........

额。。。。。。。。。。。。。。。。

为啥呢?不就是sql中in的数量多点么?肯定是多线程的问题、、、
去掉多线程,改为单线程操作!

  @Test
    public void test2(){

        AtomicInteger ai = new AtomicInteger(0);

        for(int threadId =0;threadId <50;threadId++){

            List<Long> ids = getIds(100000);
            Long aLong = userMapper.selectCnt(ids);
            ai.getAndIncrement();
            System.out.println("::开始了");

//            int finalThreadId = threadId;
//            new Thread(()->{
//                List<Long> ids = getIds(100000 + finalThreadId);
//                Long aLong = userMapper.selectCnt(ids);
//                ai.getAndIncrement();
//                System.out.println(finalThreadId +"::开始了");
//            }).start();

        }

        while (ai.get() <50){

        }
        System.out.println("结束。。。。。。。。。。");
    }

控制台打印:

::开始了
::开始了
::开始了
::开始了
::开始了
::开始了
::开始了
::开始了
::开始了
::开始了
::开始了
结束。。。。。。。。。。

没问题,没有oom,说明单个查询是没有问题的,那为啥多线程环境下就不行?显示情况下不可能是单线程查询的啊,因为接口qps你不能线程成1.。。。。。

为什么多线程下会导致OOM

我们看到Full GC的时候的detail

[Full GC (Ergonomics) [PSYoungGen: 29696K->29696K(58368K)] [ParOldGen: 170834K->170833K(175104K)] 200530K->200529K(233472K), [Metaspace: 38543K->38543K(1085440K)], 0.1151393 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded

PSYoungGen:回收前29696K,回收后29696K,没有变化
ParOldGen: 回收前170834K,回收后170833K,回收了1K
堆内存:200530K->200529K(233472K)
GC overhead limit exceeded: jvm判定垃圾回收效率过低,执行垃圾收集的时间比例太大, 有效的运算量太小. 默认情况下, 如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。

mybatis的原因

这里很多文章都归结于mybatis,Mybatis会带来OOM,主要是因为Mybatis拼接SQL的时候生成的占位符和参数对象,存放在Map里,当SQL的参数多导致SQL太长的时候,Map持有这些SQL时间较长,并且多线程同时操作,这时候内存占用就很高,从而发生OOM。
对DynamicContext类源码查看,DynamicContext又一个ContextMap 类型的参数bindings,继承了HashMap相当于一个Map集合,接着看这个类中的getBindings方法,看到了ForEachSqlNode这类调用了getBindings方法,简单的说就是ForEachSqlNode通过getBindings方法,将SQL参数和参数的占位符统一put到ContextMap这个集合里面,主要是这里面的参数和占位符无法被GC回收,并发查询量多的情况下就会导致OOM。

貌似是这个原因?

mat分析hprof文件

在这里插入图片描述
在这里插入图片描述
从hprof文件分析好像没看到mybatis的类啊,可以看到是因为线程持有内存太大,导致内存分配不够。难道说另有玄机?

测试用例分析

想了想,线程分配空间不够导致的oom,那测试用例里面哪里占用内存太大?
List ids = getIds(100000 + finalThreadId)-》掐指一算,java中Long类型占8个字节,按100000个算,1000008/1024=781.25M,而我分配了Xmx256m,新生代大小2560.33*0.8=67.584M,omg!!!
按照这个理论,我不使用mybatis的那个查询,好像也会OOM。

@Test
    public void test2() {

        AtomicInteger ai = new AtomicInteger(0);

        for (int threadId = 0; threadId < 50; threadId++) {

            int finalThreadId = threadId;
            new Thread(() -> {
                List<Long> ids = new ArrayList<>();
                for (long i = 0; i < 1000000; i++) {
                    ids.add(i);
                }
//                Long aLong = userMapper.selectCnt(ids);

                try {
                    //为了让线程持有
                    TimeUnit.SECONDS.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                ai.getAndIncrement();
                System.out.println(finalThreadId + "::开始了");
            }).start();

        }

        while (ai.get() < 50) {

        }
        System.out.println("结束。。。。。。。。。。");
    }

[Full GC (Ergonomics) [PSYoungGen: 29695K->29695K(58368K)] [ParOldGen: 174696K->174696K(175104K)] 204392K->204392K(233472K), [Metaspace: 38142K->38142K(1085440K)], 0.1527073 secs] [Times: user=1.25 sys=0.00, real=0.15 secs] 
[Full GC (Ergonomics) [PSYoungGen: 29695K->29695K(58368K)] [ParOldGen: 174697K->174697K(175104K)] 204393K->204393K(233472K), [Metaspace: 38142K->38142K(1085440K)], 0.1420339 secs] [Times: user=1.09 sys=0.00, real=0.14 secs] 
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to java_pid500480.hprof ...

好像也是OOM了!
可以看到我将代码中的mapper查询给去掉,只是在里面new了1000000个Long数字,报的是一样的错。。。所以导致oom的真凶其实不是mybatis?
测试一下把sql填长一点,看看是不是mybatis的锅?

 <select id="selectCnt" resultType="java.lang.Long">
        select count(*) from (
        <foreach collection="ids" item="id" separator=" union all ">
            select count(*) from user where id = #{id}
        </foreach>) t
    </select>
for (int threadId = 0; threadId < 50; threadId++) {

            int finalThreadId = threadId;
            new Thread(() -> {
                List<Long> ids = new ArrayList<>();
                for (long i = 0; i < 10000; i++) {
                    ids.add(i);
                }
                Long aLong = userMapper.selectCnt(ids);
                ai.getAndIncrement();
                System.out.println(finalThreadId + "::开始了");
            }).start();

        }
Cause: java.sql.SQLException: Thread stack overrun:  246176 bytes used of a 262144 byte stack, and 16000 bytes needed.  Use 'mysqld --thread_stack=#' to specify a bigger stack.
; uncategorized SQLException; SQL state [HY000]; error code [1436]; Thread stack overrun:  246176 bytes used of a 262144 byte stack, and 16000 bytes needed.  Use 'mysqld --thread_stack=#' to specify a bigger stack.; nested exception is java.sql.SQLException: Thread stack overrun:  246176 bytes used of a 262144 byte stack, and 16000 bytes needed.  Use 'mysqld --thread_stack=#' to specify a bigger stack.

好家伙,没有OOM,直接Thread stack overrun了,,这个错误很明显。sql拼接的太多了,直接超过sql语句线程的上限了,好家伙,先不管这个错,拼接短点,接着测OOM!!

for (int threadId = 0; threadId < 50; threadId++) {

            int finalThreadId = threadId;
            new Thread(() -> {
                List<Long> ids = new ArrayList<>();
                for (long i = 0; i < 10000; i++) {
                    ids.add(i);
                }
                Long aLong = userMapper.selectCnt(ids);
                ai.getAndIncrement();
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                System.out.println(finalThreadId + "::开始了");
            }).start();

        }

[Full GC (Ergonomics) [PSYoungGen: 49152K->44109K(66560K)] [ParOldGen: 173371K->175100K(175104K)] 222523K->219210K(241664K), [Metaspace: 38548K->38548K(1085440K)], 0.0577932 secs] [Times: user=0.33 sys=0.00, real=0.06 secs] 
[Full GC (Ergonomics) [PSYoungGen: 49152K->44505K(66560K)] [ParOldGen: 175100K->175013K(175104K)] 224252K->219518K(241664K), [Metaspace: 38548K->38548K(1085440K)], 0.0672780 secs] [Times: user=0.47 sys=0.00, real=0.07 secs] 
[Full GC (Ergonomics) [PSYoungGen: 49047K->44798K(66560K)] [ParOldGen: 175013K->175013K(175104K)] 224060K->219811K(241664K), [Metaspace: 38548K->38548K(1085440K)], 0.0446176 secs] [Times: user=0.27 sys=0.02, real=0.05 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid503928.hprof ...

Caused by: java.sql.SQLException: Thread stack overrun:  246176 bytes used of a 262144 byte stack, and 16000 bytes needed.  Use 'mysqld --thread_stack=#' to specify a bigger stack.

疯狂fullGC,顺便oom了。这结果好像我懂了?

结论:

其他文章中说的ContextMap确实在多线程环境下,且参数过多、查询时间较久的情况下会导致OOM,这是因为你的查询一直没有结束,那在进行垃圾回收的时候,是不会对正在运行的线程中存在的变量进行垃圾回收的(参考gcRoots节点),这就导致OOM,其他文章中说的减少sql拼接确实可以一定程度的避免OOM,但我认为问题的关键不是在ContextMap,而是在你的查询时间太长,导致不能进行垃圾回收,从而导致OOM。
但是导致OOM的原因是有很多的,比如上面的测试例子中没有进行mapper查询,依然会导致oom,所以究竟问题在哪,还是要分情况来看的,如果是入参多、查询慢,那你改了sql可能确实就没有oom了,因为是你直接缩短了查询时间,也就导致线程结束,从而垃圾进行回收。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值