场景回顾在之前的JVMFULLGC生产问题笔记中,我们提出了如何更好的实现一个多线程的消费实现。没看过的小伙伴,建议看看。本以为一切就此结束,结果又发生了一点小意外,所以记录在这里,以防自己和小伙伴们入坑。生产消费者模型介绍上一节我们尝试了多种多线程方案,总是出现各种奇怪的问题。所以最终还是决定使用生产者消费者模型来实现。实现如下:这里使用AtomicLong做一个简单的计数。userMapper.handle2(Arrays.asList(user));这个方法是同事以前的方法,当然做了很多简化。没有修改,入参是一个列表。为了兼容性,使用Arrays.asList()简单封装一下。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。Arrays;importjava.util.List;importjava.util.concurrent.*;importjava.util.concurrent.atomic.AtomicLong;/***分页查询*@authorbinbin.hou*@since1.0.0*/publicclassUserServicePageQueueimplementsUserService{//页面大小privatefinalintpageSize=10000;privatestaticfinalintTHREAD_NUM=20;privatefinalExecutorexecutor=Executors.newFixedThreadPool(THREAD_NUM);privatefinalArrayBlockingQueuequeue=newArrayBlockingQueue<>(2*pageSize,true);//模拟注入的总个数Mapper/newFixedThreadPool(THREAD_NUM);*/privateAtomicLongcounter=newAtomicLong(0);//消费者线程任务publicclassConsumerTaskimplementsRunnable{@Overridepublicvoidrun(){while(true){try{//会阻塞直到元素Useruser=queue.take();userMapper.handle2(Arrays.asList(用户));longcount=counter.incrementAndGet();}catch(InterruptedExceptione){e.printStackTrace();}}}}//初始化消费者进程//启动5个进程进行处理}}/***处理所有用户*/publicvoidhandleAllUser(){//启动消费者startConsumer();//充值计数器counter=newAtomicLong(0);//分页查询inttotal=userMapper.count();inttotalPage=total/pageSize;for(inti=1;i<=totalPage;i++){//等待消费者处理已有信息awaitQueue(pageSize);System.out.println(UserMapper.currentTime()+"第"+i+"pagequerystart");ListuserList=userMapper.selectList(i,pageSize);//直接丢入队列queue.addAll(userList);System.out.println(UserMapper.currentTime()+"上的query"+i+"页面全部完成");}}/***等到队列小于等于限制,再开始生产处理**先判断队列大小,只有当队列大了才查询可以调整为0*但是因为查询也是比较耗时的,可以在可以准备查询的时候调整到小于pageSize*,保证消费者不会等待太久*@paramlimitlimit*/privatevoidawaitQueue(intlimit){while(true){//获取阻塞队列的大小intsize=queue.size();if(size>=limit){try{//根据实际情况调整Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}else{break;}}}}测试验证当然这个方法在集成环境下运行没有问题。于是就开始直接上productionverification,结果启动很快,然后就可以慢下来了。一看GC日志,打了两次,FULLGC。该死的,一个圣人会被一招打败两次吗?FULLGC的产生一般需要fullgc的发现。最直观的感受就是程序很慢。这时候就需要加上GC日志打印,看看有没有fullgc。最糟糕的是,性能问题无法通过测试验证,除非您执行压力测试。压测还必须满足两个条件:(1)数据量足够大,或者QPS足够高。持续压力(2)资源够稀缺,就是你还想让马跑,你还想让马不吃草。幸运的是,我们同时赶上了两点钟。那么问题又来了,怎么定位为什么是FULLGC?内存泄漏程序的减速不是一开始就慢,而是开始快,然后变慢,然后就是不停的FULLGC。这自然被认为是内存泄漏。如何定位内存泄漏?可以分为以下几个步骤:(1)查看代码,看是否有明显的内存泄漏。然后修改验证。如果不能解决,找出可能存在问题的地方,进行第二步。(2)FULLGC时dump栈信息,分析哪些数据过大,然后结合1解决。接下来我们看一下这个过程的简化版本记录。问题定位看代码最基本的生产者消费者模型就算确认了,感觉也没什么问题。所以我们要看看在消费者模式下调用别人方法的问题。方法的核心目的(1)遍历入参列表,进行业务处理。(2)将当前批次的加工结果写入文件。简化版的方法实现如下:/***模拟用户处理**@paramuserList用户列表*/publicvoidhandle2(ListuserList){StringtargetDir="D:\\data\\";//理论上让每个线程只能读写自己的文件newFileWriter(fullFileName);bufferedWriter=newBufferedWriter(fileWriter);StringBufferstringBuffer=null;for(Useruser:userList){stringBuffer=newStringBuffer();//业务逻辑userExample=newUser();userExample.setId(user.getId());//如果查询结果已经存在,则跳过处理ListuserCountList=queryUserList(userExample);if(userCountList!=null&&userCountList.size()>0){return;}//其他处理逻辑//记录最终结果stringBuffer.append("User").append(user.getId()).append("同步结果完成");bufferedWriter.newLine();bufferedWriter.write(stringBuffer.toString());}//将处理结果写入文件bufferedWriter.newLine();bufferedWriter.flush();bufferedWriter.close();fileWriter.close();}catch(Exceptionexception){exception.printStackTrace();}finally{try{if(null!=bufferedWriter){bufferedWriter.close();}if(null!=fileWriter){fileWriter.close();}}catch(Exceptione){}}}这种代码怎么说,大概是祖传代码,不知道大家有没有看过,或者写过?文件部分我们可以忽略,核心部分其实只有:UseruserExample;for(Useruser:userList){//业务逻辑userExample=newUser();userExample.setId(user.getId());//如果查询结果已经存在,跳过处理ListuserCountList=queryUserList(userExample);if(userCountList!=null&&userCountList.size()>0){return;}//其他处理逻辑}代码有问题你觉得是什么问题用上面的代码?哪里可能有内存泄漏?应该如何改进?看看堆栈。如果您查看代码并确定了疑点,那么下一步就是查看堆栈并验证您的猜测。查看堆栈的方法有很多种。有很多方法可以查看jvm堆栈。这里我们以jmap命令为例。(1)查找java进程的pid,可以执行jps或psux等,选择自己喜欢的。我们在windows本地测试(实际生产一般是linux系统):D:\ProgramFiles\Java\jdk1.8.0_192\bin>jps11168Jps3440RemoteMavenServer36451211660Launcher11964UserServicePageQueueUserServicePageQueue是我们执行的测试程序,所以pid为11964(2)执行jmap获取堆栈信息的命令:jmap-histo11964效果如下:D:\ProgramFiles\Java\jdk1.8.0_192\bin>jmap-histo11964num#instances#bytesclassname--------------------------------------------1:16103120851264[C2:1579493790776java.lang.String3:17093699696[B4:34723688440[I5:1393583344592com.github.houbb。thread.demo.dal.entity.User6:1396142233824java.lang.Integer7:12716508640java.io.FileDescriptor8:12714406848java.io.FileOutputStream9:7122284880java.lang.ref.Finalizer10:12875206000java.lang.Object...你可以使用head命令过滤。当然,如果服务器不支持这个命令,可以将堆栈信息输出到一个文件:jmap-histo11964>>dump.txt堆栈分析,我们可以明显发现不合理的地方:【这里的C指的是chars,有161031.string是一个字符串,有157949个。当然还有User对象,有139358个。我们每次分页,有1W个对象,队列中的对象最多是19999个。这么多对象显然不合理。代码中的问题chars和String为什么这么多代码给人的第一印象就是在写和业务逻辑无关的文件。想必很多朋友都想到了用TWR来简化代码,但是这里有两个问题:(1)最终文件中是否可以记录所有的执行结果?(2)有没有更好的办法?对于问题1,答案是不可能的。虽然我们为每个线程创建了一个文件,但是实际测试发现文件会被覆盖。其实与其自己写文件,不如用log来记录结果,这样更优雅。所以,最后将代码简化为://LogUseruserExample;for(Useruser:userList){//业务逻辑userExample=newUser();userExample.setId(user.getId());//如果查询结果已经存在,则跳过处理ListuserCountList=queryUserList(userExample);if(userCountList!=null&&userCountList.size()>0){//log返回;}//其他处理逻辑//log记录结果}user对象为什么这里有多少?我们看一下核心业务代码:UseruserExample;for(Useruser:userList){//业务逻辑userExample=newUser();userExample.setId(user.getId());//如果查询结果已经存在,则跳过处理ListuserCountList=queryUserList(userExample);if(userCountList!=null&&userCountList.size()>0){return;}//其他处理逻辑}这里搭建一个mybatis判断是否存在常用的用户查询条件,然后判断查询列表的大小.这里有两个问题:(1)判断是否存在,最好用count而不是判断list结果的大小。(2)UseruserExample的范围要尽可能小。调整如下:for(Useruser:userList){//业务逻辑UseruserExample=newUser();userExample.setId(user.getId());//如果查询结果已经存在,则跳过处理intcount=selectCount(userExample);if(count>0){return;}//其他业务逻辑}调整代码这里System.out.println在实际使用时被log替换,这里只是为了演示。/***模拟用户处理**@paramuserList用户列表*/publicvoidhandle3(ListuserList){System.out.println("输入参数:"+userList);for(Useruser:userList){//业务逻辑UseruserExample=newUser();userExample.setId(user.getId());//如果查询结果已经存在,则跳过处理intcount=selectCount(userExample);if(count>0){System.out.println("如果查询到的结果已经存在,则跳过处理");continue;}//其他业务逻辑System.out.println("业务逻辑处理结果");}}全部改完后生产验证,重新部署验证,都很好。希望不会有第三个。:)总结当然,在验证过程中也出现了一些小插曲,比如开发者没有权限查看堆栈信息,执行命令时程序假死等等。产生fullgc是个麻烦的问题。一是难以复现,二是如果是零星的,而且是实时链接,可能不容易执行dump命令。所以代码还是写的越简单越好,不然会出现各种问题。可以尽可能复用已有的工具和中间件。从这个角度来看,我们自己写的生产-消费者模型不是很好,因为复用性不强,所以建议使用公司现有的mq工具,但是如何选择要看具体的业务场景。架构就是权衡取舍。希望本文对您有所帮助!