故事开始于早上八点,同事给我发消息。“运行批处理程序很慢,负载太高,早上帮我看看。”边走边答,一遍又一遍,看得我目瞪口呆。什么都不知道。而这就是今天整个故事的开始。问题出在公司。简单了解情况??后,我开始登录机器查看日志。看好人家,最简单的要求就是10S+,估计实时链接直接炸锅了。于是我想到了两种可能:(1)数据库有慢SQL、归档等操作严重影响性能(2)应用FULLGC,于是请DBA帮忙定位是不是第一种情况有问题,以及登录机器查看是否有FULLGC。初步解决十分钟后,DBA告诉我,确实有慢SQL,已经kill掉了。GClogs然而,查看GClogs的道路一点也不平坦。(1)发现应用本身没有打印gclog(2)想用jstat,发现docker用户没有权限,醉了。所以让plumbing帮忙重新配置jvm参数,添加gc日志。好在这个程序是批量运行的程序,可以随时发布。剩下的就等下午同事过来验证了。FULL-GC慢的根源有了GC日志后,很快定位慢是因为不断的fullgc导致的。那为什么总有fullgc呢?刚开始调整jvm配置的时候,大家觉得新一代的jvm配置太少了,于是重新调整了jvm的参数配置。结果,不幸的是,执行不久后仍然会触发fullgc。要定位fullgc的来源,只能从代码开始。CodeandRequirementsRequirements首先说说应用中需要解决的问题,比较简单。查出数据库中所有的数据,依次进行处理,但是有两点需要注意:(1)数据量比较大,有百万级别(2)单条数据的处理比较慢,希望处理的越快越好。业务简化为了让大家更容易理解,我们这里把所有的业务都简化了,用最简单的User类来模拟业务。User.java基本数据库实体。/***用户信息*@authorbinbin.hou*@since1.0.0*/publicclassUser{privateIntegerid;publicIntegergetId(){returnid;}publicvoidsetId(Integerid){this.id=id;}@OverridepublicStringtoString(){return"User{"+"id="+id+'}';}}UserMapper.java模拟数据库查询操作。publicclassUserMapper{//总数可以根据实际情况调整为100W+privatestaticfinalintTOTAL=100;publicintcount(){returnTOTAL;}publicListselectAll(){returnselectList(1,TOTAL);}publicListselectList(intpageNum,intpageSize){Listlist=newArrayList(pageSize);intstart=(pageNum-1)*pageSize;for(inti=start;iuserList=userMapper.selectAll();for(Useruser:userList){//处理单用户userMapper.handle(user);}}}这个方法非常简单易懂。但是缺点也比较大。当数据量大的时候,内存会直接炸毁。这个方法我也试过,应用直接假死,所以不可行。v2-既然paging不能加载到内存中,自然而然的就想到了paging。/***分页查询*@authorbinbin.hou*@since1.0.0*/publicclassUserServicePageimplementsUserService{/***处理所有用户*/publicvoidhandleAllUser(){UserMapperuserMapper=newUserMapper();//分页查询inttotal=userMapper.count();intpageSize=10;inttotalPage=total/pageSize;for(inti=1;i<=totalPage;i++){System.out.println("第"+i+"页查询开始");ListuserList=userMapper.selectList(i,pageSize);for(Useruser:userList){//处理单个用户userMapper.handle(user);}}}}一般这样就够了,但是因为要追求更快的处理速度,所以同事用的是多线程,实现大致如下如下。v3-pagingmultithreading这里使用Executor线程池来消费和处理单条数据。主要有两点需要注意:(1)使用sublist来控制每个线程处理的数据范围(2)使用CountDownLatch来保证当前页面处理完成后才进行下一页的查询和处理。importcom.github.houbb.thread.demo.dal.entity.User;importcom.github.houbb.thread.demo.dal.mapper.UserMapper;importcom.github.houbb.thread.demo.service.UserService;importjava.util。List;importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.Executor;importjava.util.concurrent.Executors;/***分页查询多线程*@authorbinbin.hou*@since1.0.0*/publicclassUserServicePageExecutorimplementsUserService{privatestaticfinalintTHREAD_NUM=5;privatestaticfinalExecutorEXECUTOR=Executors.newFixedThreadPool(THREAD_NUM);/***处理所有用户*/publicvoidhandleAllUser(){UserMapperuserMapper=newUserMapper();//分页查询inttotal=userMapper.count();intpageSize=10;inttotalPage=total/pageSize;for(inti=1;i<=totalPage;i++){System.out.println(""+i+"页面查询开始");ListuserList=userMapper.selectList(i,pageSize);//使用多线程处理intcount=userList.size();intcountPerThread=count/THREAD_NUM;//通过CountDownLatch确保本次分页执行完成后,再进行下一次分页处理。CountDownLatchcountDownLatch=newCountDownLatch(THREAD_NUM);for(intj=0;j{ListsubList=userList.subList(finalStartIndex,finalEndIndex);handleList(subList);//countdowncountDownLatch.countDown();});}try{countDownLatch.await();System.out.println("+i+"页面上的所有查询都已完成");}catch(InterruptedExceptione){e.printStackTrace();}}}privatevoidhandleList(ListuserList){UserMapperuserMapper=newUserMapper();//Processingfor(Useruser:userList){//处理单个用户userMapper.handle(user);}}}这个实现有点复杂,但第一感觉还是可以的。为什么是fullgc?sublist的坑这里使用了sublist的方式,性能非常好,也达到了划分范围的效果。但是一开始我怀疑这是内存泄漏的原因。SubList源码:privateclassSubListextendsAbstractListimplementsRandomAccess{privatefinalAbstractListparent;privatefinalintparentOffset;privatefinalintoffset;intsize;SubList(AbstractListparent,intoffset,intfromIndex,inttoIndex){this.parent=parent;this.parentOffset=fromIndex;.offset=offset+fromIndex;this.size=toIndex-fromIndex;this.modCount=ArrayList.this.modCount;}}可以看到SubList的原理:保存父ArrayList的引用;通过计算偏移量和大小来表示子列表在原列表范围内;由此我们可以看出,这种方式的subList保存的是对原始列表的引用,而且是强引用,导致GC回收失败,从而导致内存泄漏。当程序运行一段时间后,程序无法再申请内存,抛出内存溢出错误。解决办法是用工具代替子列表的方法。缺点是会增加内存占用,例如:/***@authorbinbin.hou*@since1.0.0*/publicclassListUtils{@SuppressWarnings("all")publicstaticListcopyList(Listlist,intstart,intend){Listresults=newArrayList();for(inti=start;i{//...});这实际上是一个语法糖,它会导致执行者引用子列表。因为执行器的生命周期很长,子列表永远不会被释放。后来代码调整如下,fullgc也确认解决。v4-pagingmulti-threadedTask我们使用Task,让子列表在任务中处理。publicclassUserServicePageExecutorTaskimplementsUserService{privatestaticfinalintTHREAD_NUM=5;privatestaticfinalExecutorEXECUTOR=Executors.newFixedThreadPool(THREAD_NUM);/***处理所有用户*/publicvoidhandleAllUser(){UserMapperuserMapper=newUserMapper();//分页查询inttotal=user;intMapper0Size();inttotalPage=total/pageSize;for(inti=1;i<=totalPage;i++){System.out.println("Page"+i+"查询开始");ListuserList=userMapper.selectList(i,pageSize);//使用多线程处理intcount=userList.size();intcountPerThread=count/THREAD_NUM;//通过CountDownLatch来保证当前页面执行完成后再继续下一页的处理。CountDownLatchcountDownLatch=newCountDownLatch(THREAD_NUM);for(intj=0;juserList){UserMapperuserMapper=newUserMapper();//处理for(Useruser:userList){//处理单个用户userMapper.handle(user);}}privateclassTaskimplementsRunnable{privatefinalCountDownLatchcountDownLatch;privatefinalListallList;privatefinalintstartIndex;privatefinalintendIndex;privateTask(CountDownLatchcountDownLatch,ListallList,intstartIndex,intendIndex){this.countDownLatch=countDownLatch;this.allList=allList;this.startIndex=startIndex;this.endIndex=endIndex;}@Overridepublicvoidrun(){试试{ListsubList=allList.subList(startIndex,endIndex);handleList(subList);}catch(Exceptionexception){exception.printStackTrace();}finally{countDownLatch.countDown();}}}}我们在这里做了一点上面没有考虑到的点,countDownLatch可能不会执行,导致线程卡死,所以我们把countDownLatch.countDown();in最后执行。努力了半天,按理说故事应该到此结束,但现实比理论更梦幻。在实际执行中,这个程序总会卡住一段时间,导致整体效果很差,还不如不应用多线程的效果。和其他同事交流后,使用生产者-消费者模型比较好,原因如下:(1)实现比较简单,不会出现奇怪的bug(2)相对于countDownLatch的强制等待,production-consumermode基本可以做到无锁,性能更好。于是,我连夜写了一个简单的demo。v5-producer-consumer模式这里我们使用ArrayBlockingQueue作为阻塞队列,它是消息的存储介质。当然你也可以使用公司的mq中间件来达到类似的效果。importcom.github.houbb.thread.demo.dal.entity.User;importcom.github.houbb.thread.demo.dal.mapper.UserMapper;importcom.github.houbb.thread.demo.service.UserService;importjava.util。List;importjava.util.concurrent.*;/***分页查询-生产消费*@authorbinbin.hou*@since1.0.0*/publicclassUserServicePageQueueimplementsUserService{//页面大小privatefinalintpageSize=10;privatestaticfinalintTHREAD_NUM=5;privatefinalExecutorexecutor=Executors.newFixedThreadPool(THREAD_NUM);privatefinalArrayBlockingQueuequeue=newArrayBlockingQueue<>(2*pageSize,true);//模拟注入privateUserMapperuserMapper=newUserMapper();//消费者线程任务publicclassConsumerTaskimplementsRunnable{@Overridepublicvoidrun(){while(true){try{//会一直阻塞到元素Useruser=queue.take();userMapper.handle(user);}catch(InterruptedExceptione){e.printStackTrace();}}}}//初始化消费者进程//启动五个进程处理privatevoidstartConsumer(){for(inti=0;iuserList=userMapper.selectList(i,pageSize);//直接抛出queue.addAll(userList);System.out.println("第"+i+"页的所有查询都完成了");}}/***等到队列小于等于限制,再进行生产处理**先判断大小queue的,可以在查询之前调整为0*但是因为查询也是比较耗时的,可以调整到小于pageSize的时候再准备查询*,这样可以保证消费者不会等待太久*@paramlimitLimit*/privatevoidawaitQueue(intlimit){while(true){//获取阻塞队列的大小intsize=queue.size();if(size>=limit){try{System.out.println("Currentsize:"+size+",limitsize:"+limit);//根据实际情况调整Thread.sleep(100);}catch(InterruptedExceptione){e.printStackTrace();}}else{break;}}}}总体实现确实简单很多,因为查询一般比处理快,所以往队列中添加元素的时候,这里有waiting。当然,您可以根据自己的实际业务调整等待时间。这里保证小于等于pageSize的时候会插入新的元素,不会超过队列的总长度。同时,消费者尽量不要进入闲置等待状态。总结一般来说,fullgc的原因一般是内存泄漏。GC日志真的很重要,遇到问题一定要记得加上,这样才能更好的分析解决问题。很多技术知识,我们自认为很熟悉,但是还是有很多坑。永远记住不要不必要地增加实体。