我的一个项目内提供了一个接口,这个接口使用异步实现,主要是做一大堆的请求并将请求到的数据更新到数据库里和记录日志,但是问题是,这个接口一旦调用并执行完毕之后,就会残留大量的字节数据在内存里且不会自动清除,大概是会产生 2G 左右的垃圾,在我的本地环境里我运行了好几次是没问题的,因为即使残留了 2G 后续也会运行会正确进行 GC 避免内存满了,我在本地环境里手动进行 GC 也是可以正确清除掉这些垃圾的>
具体看下图,我运行接口之后产生了很多的字节数据没有处理
在服务器空间足够的情况下,也能够正常进行垃圾回收从而保证项目的正常运行
但是我的服务器就只有 4G ,还要运行其他的东西,这 2G 的垃圾堆在那我内存就所剩无几了,甚至都没办法正常调用其他接口,我又没办法扩充服务器,所以想要解决这个问题
我在本地是想着先用 System.gc()这个方法来让 JVM 进行 GC 的,作为一个暂时的解决方法,但是这个方法本地能正确进行 GC ,但是到服务器就压根没做这件事,内存没多出来多少
代码因为是我学校的代码,我不能全放出来,所以我就放一部分的关键的代码到下面,我个人认为问题应该也出在下面这段代码里 业务代码:
List<OrgApiManageExecuteVO> orgApiManageList = new ArrayList<>(); // 创建一个线程池 ThreadPoolExecutor tpe = new ThreadPoolExecutor(4, 10, 40, TimeUnit.SECONDS, new LinkedBlockingQueue<>(75)); // ThreadPoolExecutor tpe = new ThreadPoolExecutor(30, 40, 40, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); log.info("api 请求开始======================="); Long startTime = System.currentTimeMillis(); for (OrgApiManage orgApiManageVo : testApiManageVos) { if (!StringUtils.hasText(orgApiManageVo.getFullClassName())) { continue; } String threadName = "org-" + orgApiManageVo.getId(); Callable<OrgApiManageExecuteVO> c = new CallApiQueryCallable(orgApiManageVo, orgApiGroupManageMap, langTypeResult, threadName); // 执行任务并获取 Future 对象 Future<OrgApiManageExecuteVO> f = tpe.submit(c); OrgFutureResult orgFutureResult = new OrgFutureResult(); orgFutureResult.setOrgApiManageVo(orgApiManageVo); orgFutureResult.setF(f); orgFutureResult.setThreadName(threadName); results.add(orgFutureResult); } log.info("========= ExecuteListSize:{} ====", results.size()); // 关闭线程池 tpe.shutdown(); // 获取所有并发任务的运行结果 for (OrgFutureResult orgFutureResult : results) { Future<OrgApiManageExecuteVO> f = orgFutureResult.getF(); if (f.isDone()) { OrgApiManageExecuteVO returnDTO = f.get(); orgApiManageList.add(returnDTO); } else { log.info("=========== 任未完成 最多等待 60 分钟 ======="); try { OrgApiManageExecuteVO returnDTO = f.get(2, TimeUnit.MINUTES); orgApiManageList.add(returnDTO); } catch (InterruptedException | ExecutionException e) { errorMsg(orgApiManageList, orgFutureResult, e, "程任意外中"); Thread.currentThread().interrupt(); } catch (TimeoutException e) { errorMsg(orgApiManageList, orgFutureResult, e, "程行的任超"); } } } log.info("======== return orgApiManageList Size:{} =====", orgApiManageList.size()); Long runTime = System.currentTimeMillis() - startTime; log.info("======= CallApiQueryCallable runTime:{}ms", runTime); return orgApiManageList;
这里的异步任务做的就是构造 URL 发送请求等待请求返回结果并进行相应的处理(比如记录或者更新),大概是这样的,只是请求的数量很多,每次请求过来的线程数有两百多个,我这小服务总是拖挺久才能搞定,然后积压一堆不知道哪里的 byte 数据在内存里还清除不掉
我因为这个问题已经困扰了两天了,我个人尝试了很多办法都没能解决,所以我来问问各位,希望有大佬能不吝赐教,或者告诉我一些简单的思路也可以,我会自己去尝试的,先谢谢各位了
1 cvbnt 312 天前 via Android 试试 spring webclient |
![]() | 2 yuaotian 312 天前 1 、任务二次分发,分成更小任务去处理 2 、流关闭要绝对放在 finally 里面或使用 try-with-resources 处理流 3 、建议重构并发任务那一块,没有具体代码,但是感觉告诉我,重构一下会解决。 |
3 palfortime 312 天前 via Android Xmx 设置了多少 |
4 letianqiu 311 天前 via iPhone 你确定你本地 jvm 的启动参数和 server 是一样的吗。System.gc()不起作用很有可能是在参数里 disableexplicitgc 了 |
![]() | 5 Badlink 311 天前 需要 60 分钟超时时间这么久吗, 缩短看看呢? |
6 orioleq 311 天前 via iPhone 线程池要等所有任务结束了才能关闭… |
7 ming159 311 天前 orgApiManageList 最后释放了吗? |
![]() | 8 LiaoMatt 311 天前 在方法中新建了线程池吗 |
9 tiRolin OP @palfortime 1g |
10 layxy 311 天前 用的哪个 http 客户端 |
11 RipperJack666 311 天前 对象创建问题 |
12 yazinnnn0 311 天前 1. 用响应式的框架+kotlin 协程 2. 用 jdk21+虚拟线程 |
13 palfortime 311 天前 via Android @tiRolin 设置 1g ,是怎么跑到 2g 的,堆外内存吗? http 请求的时候用了 netty ? http 请求用了什么库?一次请求的报文有多大? |
![]() | 14 kandaakihito 311 天前 jdk8 以及 jdk17 ,遇到的情况一模一样。 多线程跑完任务之后,一堆东西在内存里面死活 GC 不掉(资源什么的已经释放了)。而且和线程数量什么的没关系,纯粹就是运算量越大垃圾越多。堆内存也没法调小,一小就爆。 后面想了个骚操作来缓解,那就是尽可能把对象 new 在 for 循环里头,并且运算结果之类的数据全往 redis 塞,把 redis 当堆内存用( |
![]() | 15 cheng6563 311 天前 没 OOM 说明你内存没满也不需要 GC |
![]() | 16 hidemyself 311 天前 tpe 这样定义没问题吗。。 |
![]() | 17 vagusss 311 天前 每次请求都 new ThreadPoolExecutor 这不对吧 |
18 futaotao5866 311 天前 ThreadPoolExecutor tpe = new ThreadPoolExecutor(4, 10, 40, TimeUnit.SECONDS, new LinkedBlockingQueue<>(75));每次进入方法都会执行这个,这个放在方法外面 // 关闭线程池 tpe.shutdown(); 为啥要关闭,不理解,可以去掉 |
![]() | 19 jov1 311 天前 看起来线程池在业务方法里面每次创建吗,这个建议放全局,然后如果要等一批任务异步执行完,可以这样 ``` List<CompletableFuture<String>> futures = inputs.stream() .map(input -> CompletableFuture.supplyAsync(() -> process(input), executorService)) .collect(Collectors.toList()); // 等待所有任务完成并收集结果 return futures.stream().map(CompletableFuture::join).collect(Collectors.toList()); ``` 或者还是用线程池,但是用 CountDownLatch 等待任务执行都可以。 |
![]() | 20 clf 311 天前 本地可以,云端不行?是有用容器之类的部署么。。。 |
21 Plutooo 311 天前 看图和描述没感觉跟内存有多大关系,可以观察下 gc 日志或者 jstat 看下 gc 频率 线程池需要放全局算一个,线程池的参数需要调大 从你的描述中一次请求需要下发 200 个请求到下游,你的代码需要等 200 个请求都执行完成一次请求才会结束 而最大只能同时执行 14 个任务,理论情况抛开 cpu 上下文切换等耗时,你的一次请求完成时间大概是 一次请求的时间*( 200/14 ),可以大概推算下跟这个值对不对 |
22 securityCoding 311 天前 tpe.shutdown() 这里不对, 空闲时线程池会自己释放, 可以在进程退出时做退出, 你这个业务请求关肯定不对 |
23 securityCoding 311 天前 @securityCoding 看错了,我以为你的线程池是放在 class 成员变量 ,线程池不是你这么用的,在 class 创建成员变量吧? |
24 tiRolin OP @palfortime 是启动的时候参数设置为 1G ,2G 是我的本地项目启动跑到的,本地项目没做限制所以能跑到 2G 上,我刚刚做了限制之后在 2G 的内存限制下也能正确运行该接口,但是在服务器里就会占用大量内存且无法释放,没有用 netty 、用 httpclient 请求,报文大小没看 |
25 tiRolin OP @securityCoding 这个我已经修改了,现在我怀疑问题出现在配置上而不是我的代码里,因为我本地设置项目大小最大为 2G 的情况下也可以正确运行该接口,但是在服务器里却会占用大量内存且无法释放 |
26 tiRolin OP @tiRolin 好吧,刚刚太着急说了,虽然能正常运行,但是执行完后也会产生大量没回收的垃圾在内存里,大概是 1G 左右,不手动回收的话就不回收了,还是跟之前一样的情况 |
![]() | 27 zsj1029 311 天前 曾经出现过 vcenter 的虚机执行 dotnet 程序,内存泄漏 重点是 dotnet 的程序运行完都退出了,堆内存一直不释放,进程管理器,根本找不到内存占用进程 无论是 windows 还是 linux 一样的情况 最后排查下来 vcenter 的问题,因为本地运行和 kvm 虚机都是正常的 |
![]() | 28 nice2cu 311 天前 线程池是每请求一次就创建一个线程池? |
29 palfortime 311 天前 via Android @tiRolin 你不是说服务器上用了 2g 吗?你直接贴服务器的驱动命令好了。jdk 什么版本。 |
30 assiadamo 311 天前 自己的小东西,服务器配置又小,可以试试 go ,相同的业务可以省很多内存 |
31 chihiro2014 311 天前 写并发的时候,不是应该避免重复创建线程池这种玩意么。你这样调用一次,就创一次。。oom 不是很正常? |
![]() | 32 Dream95 311 天前 JVM GC 后不一定是把内存归还给操作系统的 |
![]() | 33 zoharSoul 311 天前 没 oom 说明不用 gc |
![]() | 34 rb6221 311 天前 有几种可能 1. 代码有问题,建议丢给 AI 帮你优化 2. 没用框架,用的原始 api 手撸的,有条件的话建议用框架 3. 是很大,但是在合理范围内,没法再优化了,这种只能换语言或者升级机器 |
![]() | 35 weenhall5 311 天前 new ArrayList<>()指定初始化大小,看你这个数据量估计一直扩容也有影响 |
36 julyclyde 311 天前 @kandaakihito 你的情况跟人家不一样啊。人家的可以 gc |
![]() | 37 hdfg159 311 天前 写一个简单能运行复现问题的 demo |
38 wwalkingg 311 天前 用 Kotlin 协程 |
![]() | 39 blackmamba24 311 天前 @chihiro2014 在用户线程里创建线程池我是首次见 |
40 kneo 311 天前 via Android 旧版本堆增长之后就是不缩小的。你可以升级到最新的 jdk 试试。 |
41 chihiro2014 311 天前 @blackmamba24 一般的做法不是创建全局线程池,然后共享么=。=。。。他这个方法结束了,线程池也 g |
![]() | 42 sagaxu 311 天前 1. JVM 版本未知,从 Java 11 开始,G1 GC 之后才可能把内存归还给 OS ,从 OS 层面不一定能观测到释放内存。 2. 缺乏 GC 日志,堆和堆外内存使用状况,必须结合 GC 日志才能分析,这是关键中的关键。 3. “甚至都没办法正常调用其他接口”,从描述看是 GC 也回收不了,有长生命周期变量持有废弃数据? 4. 观测一下线程数量,是否有太多线程没有正常结束。 5. 线程池要复用,不要老去创建新的。 |
43 tiRolin OP @yuaotian 谢谢了,我的问题已经解决了,其实跟内存无关,是跟 CPU 资源有关,内存是可以正确回收的,CPU 资源被占用了太多导致无法访问,实在不好意思,我的思路错误了所以我一楼写的内容也错误了,导致给大伙们带来了错误的思路 |
![]() | 44 blackmamba24 290 天前 @chihiro2014 正解 |