文章目录
我总结一次OOM异常处理情况,附一个OOM demo。
package com.example.demo.service.jvm;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
public class HeapMemoryUtils {
/**
*-Xmx8m -Xms8m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:tmp
*/
static class HeapObject {
}
public static void getMemory(){
long maxMemory = Runtime.getRuntime().maxMemory();
long memory = maxMemory/1024/1024;
System.out.println(memory);
}
public static void main(String[] args) {
List<HeapObject> memorys = new ArrayList<>();
while (true){
memorys.add(new HeapObject());
HeapMemoryUtils.getMemory();
}
}
}
报错:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:267)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
at java.util.ArrayList.add(ArrayList.java:464)
at com.example.demo.service.jvm.HeapMemoryUtils.main(HeapMemoryUtils.java:22)
对于OOM情况,设置这这个参数-Xmx8m -Xms8m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:tmp 可以本地跑出来Dump文件,用工具去分析,会得到一个内存占用,类似下图:
这个图会详细的描述所有的内存占用,主要关注内存最大的块。
而在我们的业务场景中,举个例子,我们的一个表有100多个字段,采用List《String》接收,100多个字段,String占用的话就是几百个字符,一个对象就是1K,1000个对象就是1M,1000*1000个就是1g,这才是百万数据的存储。千万级数据拿到内存中就是10G ,我们整个服务器的内存才是8G,直接打爆内存了,OOM异常了。所以就需要解决这个问题。
JVM设置调优 8G内存举例
Xmx Xms
◦-Xmx:确定在生产环境下(server模式,64位Centos,JRE 8),Xmx的默认值可以采用以下规则计算:
▪容器内存小于等于2G:默认值为容器内存的1/2,最小16MB, 最大512MB。
▪容器内存大于2G:默认值为容器内存的1/4, 最大可到达32G。
◦-Xms: 默认值为容器内存的1/64, 最小8MB,如果明确指定了Xmx并且小于容器内存1/64, Xms默认值为Xmx指定的值。
◦-NewRatio: 默认2,即年轻代和年老代的比例为1:2, 年轻代大小为堆内内存的1/3。大的内存对象是直接放到老年代的。我们上述的20万数据就是2G 那你首先要保证这个2G数据在老年代是可以直接放下的。所以我们项目的设置是Xmx Xms的都是4096m,
NewRatio设置为1:3,那年轻代和年老代的各占用1G和3G,这样保证了不会频繁进行GC.
当系统内存不够用了,就会发生GC来获取可用的内存空间,其次我们要监视整个的GC情况。
使用Jstate 命令监视调整整个jvm的线程优化问题。
G1垃圾回收器的优点
我们业务采用了采用了G1垃圾回收器
1.高效性:G1垃圾回收器能够在短时间内完成垃圾回收,这使得它在高吞吐量应用程序中表现出色。它可以与大堆一起快速清理,并支持并发标记,这对于需要频繁执行垃圾回收的应用程序来说是一个优势。:-XX:ConcGCThreads=n
2.可预测性:G1垃圾回收器采用了基于时间的回收策略,这意味着它能在固定的时间窗口内执行垃圾回收,提高了应用的稳定性。 -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200毫秒
3.可扩展性:G1垃圾回收器能够在多个处理器之间并行操作,这样可以提高整体的处理效率,尤其适用于多核处理器环境下的应用程序。
4.内存占用较低:与其他垃圾回收器相比,G1垃圾回收器不需要过多的内存来存储标记和信息,因为它使用了标记压缩技术,减少了内存碎片的产生。12
5.对于String类型的优化
G1垃圾回收器的缺点:
启动时间长:由于需要进行堆空间的分区,G1垃圾回收器的启动时间相对较长。
占用内存较高:为了提供上述的优点,G1垃圾回收器需要在内存中保留更多的数据结构,如标记信息和回收状态信息,这可能会增加系统的内存需求。
不适合小内存环境:在较小的内存配置下,G1可能无法提供足够的性能优势,尤其是当与CMS垃圾回收器比较时
G1垃圾回收器因其并行和并发特性、分代收集策略以及对内存空间的整合而受到欢迎
-XX:+UseG1GC 使用G1垃圾收集器 -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200毫秒。 -XX:G1HeapRegionSize=n 设置的G1区域的大小。值时2的幂,范围是1MB到32MB之间。目标是根据最小的Java堆大小划分出约2048个区域。 默认是堆内存的1/2000。 设置并行标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。 -XX:InitiatingHeapOccupancyPercent=n 设置触发标记周期的Java堆占有率阀值。默认占用率是整个Java堆的45%。原因是
代码调优
1.上面我们提到的用List去查询,没行有100多个字段,内存占用比较大,那我们可以把这个字段进行拆分,100个太多了那我们就用50个或者更少,这就是数据库的垂直拆分或者水平拆分。主要是解决数据量过大的问题
2.还有一些引用 是我们在YGC时清理的对象,本来想说在循环外创建对象的示例的 ,但是查了一下 现在的JVM优化对于这个已经没有影响了。。这个就略过了。但是在最后使用结束以后加一行 ==null 可以避免很多问题。
3.在以前的jdk7中 使用String+会不停地创建新的对象,这个当时我们也遇到过,在1.8已经解决了。
4.引入redis缓存,特别适用于重复计算的场景。
5.多线程优化,大家使用线程的方法各式各样,造成了极大的资源浪费。所以我们强制所有的线程创建都是使用资源池,资源池是一个单例模式的, 使用线程池+生产消费者模型
6.对于各种流的使用后需要关闭,不关闭可能会造成内存泄漏
CMS(Concurrent Mark Sweep)和G1(Garbage First)都是Java虚拟机中的垃圾回收器。它们有以下区别:
内存占用不同:CMS垃圾回收器在执行垃圾回收时,会暂停应用程序的执行,因此它的内存占用相对较低;而G1垃圾回收器则不会暂停应用程序的执行,但它需要更多的内存来存储堆空间中的对象。
垃圾收集方式不同:CMS垃圾回收器使用标记清除算法来收集垃圾,这种算法可以快速地清理老年代中的无用对象;而G1垃圾回收器使用分代算法,将堆空间分为多个区域,分别进行垃圾收集,并根据区域中的对象分布情况来进行垃圾收集。
垃圾回收时间不同:由于G1垃圾回收器使用分代算法和增量整理策略,因此它的垃圾回收时间相对较长;而CMS垃圾回收器则可以在短时间内完成垃圾回收。
并发性不同:CMS垃圾回收器在进行垃圾回收时会暂停应用程序的执行,因此它的并发性较差;而G1垃圾回收器则不会暂停应用程序的执行,因此它的并发性较好。
jdk8的默认垃圾回收器 –参考
使用
jstat -gc 进程ID 查看gc运行情况
总结
- 业务背景,我们是在一个什么业务背景,产生了OOM问题,先去日志里面定位这个问题,然后拿到eks里面去分析,因为我们需要处理大量的数据, 2. 03版本的xls限制6万5,07版本的限制104万,而csv格式的没有限制,就更大了几百M都可能,一行数据一百多行,100万加到内存就爆了。后来我们优化了代码,限制了每次读取的行数,但是效率特别慢
。好处是没有oom了。再后来我们发现了一中新的方法,我们以前使用的数据库是mysql,后来改为了pg,pg和mysql的不同:使用pg自带的命令 copy from,这个数据导入特别快,100万的数据10秒内就结束,而且支持数据。所以这个数据导入的问题解决了。
- 我们是如何检测的:首先我们加上这两个参数,-XX:+HeapDumpOnOutOfMemoryError - XX:HeapDumpPath=D:tmp ,可以打印dump文件;再其次 我们使用top和jstate -gc 命令,可以查看 gC运行情况,主要关注OC OU 还有FGC这几个参数,可以看到老年代内存使用基本用完了。我们就设置了xmx xms的大小,设置了老年代和edon的比例,后来优化了使用G1垃圾回收器。我们还查看了cpu占用,发现了内存占用比较高。发现大家在开发时使用线程都是各式各样的,严重浪费资源。所以规定了使用线程池,单例模式。
- 最后优化的话,还有一部分的分库分表操作,因为一行数据太大了,使用了垂直分表和水平分表,加了缓存,采用了生产消费者模型。