java程序出现oom如何解决?什么场景下会出现oom?

本文详细介绍了Java运行时可能出现的四种内存溢出情况:堆内存溢出、方法区与元空间溢出、直接内存溢出和栈内存溢出,并通过代码示例进行演示。针对每种情况,提供了相应的排查方法,如使用内存转储快照分析工具MAT,以及调整JVM参数。此外,还探讨了不同Java版本中内存区域的变化对异常的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.堆内存溢出

堆内存溢出太常见,⼤部分⼈都应该能想得到这⼀点,堆内存⽤来存储对象实例,我们只要不停的创建对象,并且保证GC Roots和对象之间有可达路径避免垃圾回收,那么在对象数量超过最⼤堆的⼤⼩限制后很快就能出现这个异常。
写⼀段代码测试⼀下,设置堆内存⼤⼩2M。
在这里插入图片描述

public class HeapOOM {
	public static void main(String[] args) 
		{List<HeapOOM> list = new ArrayList<>(); 
		while (true) {
			list.add(new HeapOOM());
		}
	}
}

运⾏代码,很快能看见OOM异常出现,这⾥的提⽰是Java heap space堆内存溢出。
在这里插入图片描述

⼀般的排查⽅式可以通过设置-XX: +HeapDumpOnOutOfMemoryError在发⽣异常时dump出当前的内存转储快照来分析,分析可以使
⽤Eclipse Memory Analyzer(MAT)来分析,独⽴⽂件可以在官⽹下载。

另外如果使⽤的是IDEA的话,可以使⽤商业版JProfiler或者开源版本的JVM-Profiler,ft外IDEA2018版本之后内置了分析⼯具,包括Flame Graph(⽕焰图)和Call Tree(调⽤树)功能。
在这里插入图片描述

⽕焰图

2.⽅法区(运⾏时常量池)和元空间溢出

⽅法区和堆⼀样,是线程共享的区域,包含Class⽂件信息、运⾏时常量池、常量池,运⾏时常量池和常量池的主要区别是具备动态性,也 就是不⼀定⾮要是在Class⽂件中的常量池中的内容才能进⼊运⾏时常量池,运⾏期间也可以可以将新的常量放⼊池中,⽐如String的intern()⽅法。

我们写⼀段代码验证⼀下String.intern(),同时我们设置-XX:MetaspaceSize=50m -XX:MaxMetaspaceSize=50m 元空间⼤⼩。由于我使⽤的是1.8版本的JDK,⽽1.8版本之前⽅法区存在于永久代(PermGen),1.8之后取消了永久代的概念,转为元空间(Metaspace),如果是之前版本可以设置PermSize MaxPermSize永久代的⼤⼩。

private static String str = "test";
	public static void main(String[] args) 
		{List<String> list = new 
		ArrayList<>(); while (true){
			String str2 = str + str; str = str2; 
			list.add(str.intern());
		}
}

运⾏代码,会发现代码报错。
在这里插入图片描述

再次修改配置,去除元空间限制,修改堆内存⼤⼩-Xms20m -Xmx20m,可以看见堆内存报错。
在这里插入图片描述
这是为什么呢?intern()本⾝是⼀个native⽅法,它的作⽤是:如果字符串常量池中已经包含⼀个等 于ftString对象的字符串,则返回代表池中这个字符串的String对象;否则,将ftString对象包含的字符串添加到常量池中,并且返回String对象的引⽤。
⽽在1.7版本之后,字符串常量池已经转移到堆区,所以会报出堆内存溢出的错误,如果1.7之前版本的话会看见PermGen space的报错。

3.直接内存溢出

直接内存并不是虚拟机运⾏时数据区域的⼀部分,并且不受堆内存的限制,但是受到机器内存⼤⼩的限制。常见的⽐如在NIO中可以使⽤native函数直接分配堆外内存就容易导致OOM的问题。
直接内存⼤⼩可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java 堆最⼤值-Xmx⼀样。

由直接内存导致的内存溢出,⼀个明显的特征是在Dump⽂件中不会看见明显的异常,如果发现OOM之后Dump⽂件很⼩,⽽程序中⼜直 接或间接使⽤了NIO,那就可以考虑检查⼀下是不是这⽅⾯的原因。

4.栈内存溢出

栈是线程私有,它的⽣命周期和线程相同。每个⽅法在执⾏的同时都会创建⼀个栈帧⽤于存储局部变量表、操作数栈、动态链接、⽅法出⼝ 等信息,⽅法调⽤的过程就是栈帧⼊栈和出栈的过程。
在java虚拟机规范中,对虚拟机栈定义了两种异常:
如果线程请求的栈深度⼤于虚拟机所允许的深度,将抛出StackOverflowError异常
如果虚拟机栈可以动态扩展,并且扩展时⽆法申请到⾜够的内存,抛出OutOfMemoryError异常先写⼀段代码测试⼀下,设置-Xss160k,-Xss代表每个线程的栈内存⼤⼩

public class StackOOM 
	{ private int length = 1;

	public void stackTest()
		{ System.out.println("stack lenght=" +
		length); length++;
		stackTest();
	}


	public static void main(String[] args) 
	{ StackOOM test = new 
	StackOOM(); test.stackTest();
	}
}

测试发现,单线程下⽆论怎么设置参数都是StackOverflow异常。
在这里插入图片描述

尝试把代码修改为多线程,调整-Xss2m,因为为每个线程分配的内存越⼤,栈空间可容纳的线程数量越少,越容易产⽣内存溢出。反之,如果内存不够的情况,可以调⼩该参数来达到⽀撑更多线程的⽬的。

在这里插入图片描述

-多个线程的几种实现方式包括:承Thread类,实现Runnable接口,实Callable接口,使用线程池。 - Java中的线程池是通过ThreadPoolExecutor类实现的。线程池维护了一个线程队列,可以复用线程,减少线程的创建和销毁开销,提高了性能。 - 不建议直接使用Executors工具类创建线程池是因为它使用的是默认的线程池配置,可能导致线程数量过多,耗尽系统资源。OOM(Out of Memory)是由于创建过多的线程导致内存不足而发生的错误。 - Java内存模型(JMM)是一种规范,定义了多线程程序中各个变量的访问方式。它包括主内存和工作内存,通过控制变量的可见性和原子性来保证线程间的通信与同步。 - 并发编程可能会发生的问题包括:竞态条件、死锁、活锁、饥饿等。可见性问题指一个线程对共享变量的修改对其他线程是否可见,原子性问题指一个操作是否可以被中断或者同时执行。 - 并发编程下会出现原子性问题是因为多个线程同时修改同一个共享变量时,可能会导致不一致的结果。有序性问题是指程序执行的顺序与预期不符。可以使用synchronized关键字、Lock锁等来解决原子性和有序性问题。加上volatile关键字可以保证可见性,禁止指令重排序。 - 内存屏障是通过编译器和处理器来实现的,用于控制指令的执行顺序和内存的可见性。synchronized关键字会在进入和退出临界区时加上内存屏障。 - 单线程指令重排在不影响单线程执行结果的前提下进行优化,但可能会影响多线程的正确性。双重校验锁中使用volatile是为了禁止指令重排,确保多线程环境下的正确性。 - InnoDB的索引是通过B+树实现的。B+树具有树高度低、查询效率高、支持范围查询等优势。 - 聚簇索引与非聚簇索引的区别在于数据的存储方式。聚簇索引将数据行存储在叶子节点中,非聚簇索引则将叶子节点指向数据行。不是所有情况都需要取回表的数据,可以通过覆盖索引来避免回表操作。 - 最左前缀匹配指在使用联合索引时,只有从左到右使用索引的前缀部分才能发挥索引的作用。将区分度高的字段放在最左边可以提高索引的效率。唯一索引与普通索引的区别在于是否允许重复值。 - 排查慢SQL可以通过查看慢查询日志、使用性能分析工具(如EXPLAIN、SHOW PROFILE)、优化查询语句等方法。 - MySQL的锁包括行锁和表锁。行锁在并发性能上更好,但需要更多的系统资源,适合处理并发访问较高的场景。表锁在资源消耗上较少,但并发性能相对较差,适合处理并发访问较低的场景。 - FOR UPDATE语句会对查询到的行加上行锁。 - 悲观锁是指在操作数据时始终假设会发生并发冲突,因此会将数据加锁以阻止其他事务的访问。乐观锁是指不加锁,而是通过版本号或时间戳等机制来判断是否发生冲突,减少了加锁的开销。悲观锁适用于并发冲突较多的场景,乐观锁适用于并发冲突较少的场景
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值