某天在1C1G小实例上定时跑的任务没有执行,查看日志发现OOM了。
之前写过JAVA Dump小连招,但更偏重于死锁等问题,本文偏重于内存OOM问题。
日志如下:
1 2 2025-01-10 11:55:01.155 INFO 7036 --- [ol-224-thread-1] c.p.bs.service.impl.ScheduleServiceImpl : fetchAndSaveStockFundFlow schedule job start Exception in thread "pool-224-thread-1" java.lang.OutOfMemoryError: Java heap space
当时代码写得比较暴力,线程池没有命名、一次性将相关数据取出、调用远程服务获取数据、然后一次性灌入数据库中。
因此,闭着眼睛也能猜到1G的小实例默认只分配300M左右堆内存,又加上每个任务设置了5个线程同时操作,所以就算没有内存泄露,OOM也只是时间问题。
DUMP 第一步需要将内存快照dump下来,一般使用jmap或jhsdb操作。
1 jmap -dump:live,format=b,file=heapDump.hprof [PID]
1 jhsdb jmap --binaryheap --pid [PID]
虽然其他博主建议使用jhsdb,但执行的时候也OOM了,实属套娃。
执行jamp后,在执行位置获得了heapDump.hprof文件。
接下来但得想办法把文件从服务器导出,方法有很多种,scp也可以。
1 scp username@servername:/remote_path/filename ~/local_destination
也可以用Nginx搭一个文件服务器,方便随时下载其他数据。(注意数据安全问题,不要在目录中存放敏感数据,内存dump也会暴露敏感数据)
Nginx文件服务器搭建 起手安装nginx
创建文件服务器使用的公开文件夹
注意:请不要在/root文件夹下新建服务器文件夹,会有403权限问题。
样例报错:
1 2025/01/10 15:25:15 [error] 30497#30497: *18 "/root/fileroot/index.html" is forbidden (13: Permission denied), client: IP, server: , request: "GET / HTTP/1.1", host: "IP"
修改权限和所有者,优于默认是root用户,就不加sudo了。
1 2 3 chown -R nginx:nginx /var/www/ chmod -R 755 /var/www/
创建nginx配置:/etc/nginx/conf.d/file.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server { listen 80 ; location / { root /var/www/; autoindex on ; autoindex_exact_size off ; autoindex_localtime on ; add_header 'Access-Control-Allow-Origin' '*' ; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' ; add_header 'Access-Control-Allow-Headers' 'Authorization' ; access_log /var/log/nginx/access.log; } }
启动服务
将第一步dump结果移动到/var/www文件夹下,打开浏览器,输入ip地址即可看到对应的文件夹内容。
如果是一次性服务器,下载后使用systemctl stop nginx关闭nginx即可。
解析 解析工具很多,但Intellij IDE能直接打开并分析hprof文件。
解析hprof
能够看到哪些DTO数量比较多,很显然,问题就出现在不断地【一次性将相关数据取出】,产生了太多的StockBaseInfoEntity,占用159.8MB。上面也说了,JAVA默认的最大堆内存为机器内存的25%,所以很容易就OOM了。
代码优化 公共数据静态化 由于多个任务都需要同一份公共数据,而原有的代码中会不断全量拉取公共数据,因此导致内存不断膨胀。
因此,将公共数据静态化,多次读取操作指向同一块内存。
但也需要注意 1.工程冷启动时需要初始化数据、2.线程安全问题。
批量分片 将全部查出、全部处理、全部写入改为每1K或是1百条操作一次。
可以用guava直接分片,这里不新增依赖,使用原生的for方法:
1 2 3 4 5 6 7 for (int i = 0 ; i < stockBaseInfoEntities.size(); i += Base.BATCH_SIZE) { Date lowestDate = fetchService.fetchAndSaveBsPointInstruct(stockBaseInfoEntities.subList(i, Math.min(i + Base.BATCH_SIZE, stockBaseInfoEntities.size()))); if (lowestDate == null ) { continue ; } fetchService.calcLastSignalDaysAndCloseDiff(); }
线程池命名 这里直接用了hutool的NamedThreadFactory类。
1 private final ExecutorService instructExecutorService = Executors.newFixedThreadPool(BsInstruct.THREAD_NUM, new NamedThreadFactory("InstructExecutor" , false ));
修改jar运行参数 调整堆大小:
1 java -Xms128m -Xmx750m -jar yourapp.jar
新增GC相关参数:
1 java -Xms128m -Xmx750m -XX:+PrintGCDetails -jar yourapp.jar
优化后 338MB → 59.6MB
优化后
附:JAVA额外参数 使用java -X,能看到所有的额外参数和解释。
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 java -X -Xbatch 禁用后台编译 -Xbootclasspath/a:<以 : 分隔的目录和 zip/jar 文件> 附加在引导类路径末尾 -Xcheck:jni 对 JNI 函数执行其他检查 -Xcomp 强制在首次调用时编译方法 -Xdebug 不执行任何操作;已过时,将在未来发行版中删除。 -Xdiag 显示附加诊断消息 -Xfuture 启用最严格的检查,预期将来的默认值。 此选项已过时,可能会在 未来发行版中删除。 -Xint 仅解释模式执行 -Xinternalversion 显示比 -version 选项更详细的 JVM 版本信息 -Xlog:<opts> 配置或启用采用 Java 虚拟 机 (Java Virtual Machine, JVM) 统一记录框架进行事件记录。使用 -Xlog:help 可了解详细信息。 -Xloggc:<file> 将 GC 状态记录在文件中(带时间戳)。 此选项已过时,可能会在 将来的发行版中删除。它将替换为 -Xlog:gc:<file>。 -Xmixed 混合模式执行(默认值) -Xmn<size> 为年轻代(新生代)设置初始和最大堆大小 (以字节为单位) -Xms<size> 设置初始 Java 堆大小 -Xmx<size> 设置最大 Java 堆大小 -Xnoclassgc 禁用类垃圾收集 -Xrs 减少 Java/VM 对操作系统信号的使用(请参见文档) -Xshare:auto 在可能的情况下使用共享类数据(默认值) -Xshare:off 不尝试使用共享类数据 -Xshare:on 要求使用共享类数据,否则将失败。 这是一个测试选项,可能导致间歇性 故障。不应在生产环境中使用它。 -XshowSettings 显示所有设置并继续 -XshowSettings:all 显示所有设置并继续 -XshowSettings:locale 显示所有与区域设置相关的设置并继续 -XshowSettings:properties 显示所有属性设置并继续 -XshowSettings:vm 显示所有与 vm 相关的设置并继续 -XshowSettings:security 显示所有安全设置并继续 -XshowSettings:security:all 显示所有安全设置并继续 -XshowSettings:security:properties 显示安全属性并继续 -XshowSettings:security:providers 显示静态安全提供方设置并继续 -XshowSettings:security:tls 显示与 TLS 相关的安全设置并继续 -XshowSettings:system (仅 Linux)显示主机系统或容器 配置并继续 -Xss<size> 设置 Java 线程堆栈大小 实际大小可以舍入到 操作系统要求的系统页面大小的倍数。 -Xverify 设置字节码验证器的模式 请注意,选项 -Xverify:none 已过时, 可能会在未来发行版中删除。 --add-reads <module>=<target-module>(,<target-module>)* 更新 <module> 以读取 <target-module>,而无论 模块如何声明。 <target-module> 可以是 ALL-UNNAMED,将读取所有未命名 模块。 --add-exports <module>/<package>=<target-module>(,<target-module>)* 更新 <module> 以将 <package> 导出到 <target-module>, 而无论模块如何声明。 <target-module> 可以是 ALL-UNNAMED,将导出到所有 未命名模块。 --add-opens <module>/<package>=<target-module>(,<target-module>)* 更新 <module> 以在 <target-module> 中打开 <package>,而无论模块如何声明。 --limit-modules <module name>[,<module name>...] 限制可观察模块的领域 --patch-module <module>=<file>(:<file>)* 使用 JAR 文件或目录中的类和资源 覆盖或增强模块。 --source <version> 设置源文件模式中源的版本。 --finalization=<value> 控制 JVM 是否执行对象最终处理, 其中 <value> 为 "enabled" 或 "disabled" 之一。 默认情况下,最终处理处于启用状态。
GC日志相关选项,转自 https://www.cnblogs.com/dupengpeng/p/17620200.html :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 -XX:+PrintGC <==> -verbose:gc 打印简要日志信息 -XX:+PrintGCDetails 打印详细日志信息 -XX:+PrintGCTimeStamps 打印程序启动到GC发生的时间,搭配-XX:+PrintGCDetails使用 -XX:+PrintGCDateStamps 打印GC发生时的时间戳,搭配-XX:+PrintGCDetails使用 -XX:+PrintHeapAtGC 打印GC前后的堆信息,如下图 -Xloggc:<file> 输出GC导指定路径下的文件中 -XX:+TraceClassLoading 监控类的加载 -XX:+PrintGCApplicationStoppedTime 打印GC时线程的停顿时间 -XX:+PrintGCApplicationConcurrentTime 打印垃圾收集之前应用未中断的执行时间 -XX:+PrintReferenceGC 打印回收了多少种不同引用类型的引用 -XX:+PrintTenuringDistribution 打印JVM在每次MinorGC后当前使用的Survivor中对象的年龄分布 -XX:+UseGCLogFileRotation 启用GC日志文件的自动转储 -XX:NumberOfGCLogFiles=1 设置GC日志文件的循环数目 -XX:GCLogFileSize=1M 设置GC日志文件的大小
附2:jhsdb jmap查看堆信息 1 jhsdb jmap --pid [PID] --heap
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 46 47 Attaching to process ID 31724, please wait... Debugger attached successfully. Server compiler detected. JVM version is 21.0.5+9-LTS-239 using thread-local object allocation. Mark Sweep Compact GC Heap Configuration: MinHeapFreeRatio = 40 MaxHeapFreeRatio = 70 MaxHeapSize = 786432000 (750.0MB) NewSize = 44695552 (42.625MB) MaxNewSize = 262144000 (250.0MB) OldSize = 89522176 (85.375MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 22020096 (21.0MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB Heap Usage: New Generation (Eden + 1 Survivor Space): capacity = 63569920 (60.625MB) used = 972208 (0.9271697998046875MB) free = 62597712 (59.69783020019531MB) 1.5293522471005154% used Eden Space: capacity = 56557568 (53.9375MB) used = 972208 (0.9271697998046875MB) free = 55585360 (53.01033020019531MB) 1.7189706601245656% used From Space: capacity = 7012352 (6.6875MB) used = 0 (0.0MB) free = 7012352 (6.6875MB) 0.0% used To Space: capacity = 7012352 (6.6875MB) used = 0 (0.0MB) free = 7012352 (6.6875MB) 0.0% used tenured generation: capacity = 140910592 (134.3828125MB) used = 38475656 (36.69324493408203MB) free = 102434936 (97.68956756591797MB) 27.305013380399394% used
附3:jhsdb jmap histo查看class内存情况 按照内存使用量逆序排列,注意会打印一堆信息,建议带上more。
1 jhsdb jmap --pid [PID] --histo | more
前面一堆信息中只有一个眼熟的。这个entity就是上文说的静态化对象,因此内存泄露问题解决。
histo