Java技术专题-JVM研究系列(26)让你完全攻克内存溢出(OOM)这一难题
每日一句
只有经历地狱般的磨练,才能创造出天堂般的力量。
堆(Heap)内存不足
报错信息:
java.lang.OutOfMemoryError: Java heap space
导致原因
-
代码中可能存在大对象分配
-
可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
-
业务场景会剧增对象数据,应该提升内存空间。
解决方法
-
检查是否存在大对象的分配,最有可能的是大数组分配
-
通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
-
如果没有找到明显的内存泄露,使用 -Xms/-Xmx 加大堆内存
-
还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
方法区溢出
报错信息:
java.lang.OutOfMemoryError: PermGen space java.lang.OutOfMemoryError: Metaspace
导致原因
-
JDK8之前,永久代是HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。
-
JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:
- 字符串常量由永久代转移到堆中
- 和永久代相关的JVM参数已移除
-
出现永久代或元空间的溢出的原因可能有如下几种:
- 在Java7之前,频繁的错误使用String.intern方法。
- 生成了大量的代理类,导致方法区被撑爆,无法卸载。
- 应用长时间运行,没有重启。
解决方法
-
永久代/元空间 溢出的原因比较简单,解决方法有如下几种:
-
检查是否永久代空间或者元空间设置的过小。
-
检查代码中是否存在大量的反射操作或者class加载操作以及生产class字节码。
-
dump之后通过mat检查是否存在大量由于反射生成的代理类
-
放大招,重启JVM
-
GC overhead limit exceeded
报错信息
java.lang.OutOfMemoryError:GC overhead limit exceeded
导致原因
这个是JDK6新加的错误类型,一般都是堆太小导致的。
Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
解决方法
-
检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
-
添加参数-XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
-
dump内存,检查是否存在内存泄露,如果没有,加大内存。
虚拟机栈和本地方法栈溢出
由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
-
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
-
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里把异常分成两种情况看似更加严谨,但却存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
虚拟机的 StackOverflowError 异常
-Xss参数减小栈内存的容量,然后不断调用方法造成栈溢出,StackOverflowError 异常。
public class JVMStackSOF { private int stacklength = 1; // 记录栈深度 // 调用这个递归方法以造成栈溢出 public void stackPush(){ stacklength++; stackPush(); } public static void main(String[] args) throws Throwable{ JVMStackSOF sof = new JVMStackSOF(); try{ sof.stackPush(); }catch(Throwable e){ System.out.println("stack length = " + sof.stacklength); throw e; } } }
openjdk@ubuntu:~$ java -Xss256k -cp /home/openjdk/NetBeansProjects/JavaApplication1/build/classes test_JVMStackSOF.JVMStackSOF stack length = 1888 Exception in thread "main" java.lang.StackOverflowError at test_JVMStackSOF.JVMStackSOF.stackPush(JVMStackSOF.java:17) at test_JVMStackSOF.JVMStackSOF.stackPush(JVMStackSOF.java:18)
-Xss256K:设置参数栈内存容量为256K
- 在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常,异常出现时输出的栈深度相应缩小。
定义了大量的本地变量,增加此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常时输出的栈深度相应缩小。
虚拟机栈隔离的,每个线程都有自己独立的虚拟机栈。
在 Java 虚拟机规范中,对虚拟机栈这个区域规定了两种异常状况:
-
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;
-
如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展),在扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。
虚拟机的 OutOfMemoryError 异常
通过-Xss2M参数增大栈内存的容量,然后不断开启新的线程,抛出OutOfMemoryError 异常。
public class JVMStackOOM { private void dontStop() { while (true) { } } public static void main(String[] args) { // 不断开启新的线程消耗虚拟机栈空间 while (true) { new Thread(new Runnable() { @Override public void run() { dontStop(); } }).start(); } } }
原理
- 主要是因为-Xss参数设置的是一个线程的栈大小,前面已经说过虚拟机栈是线程私有的,即每个线程都有一个自己的栈。
操作系统分配给每个进程的内存是有限制的,譬如 32 位的 Windows 限制为 2GB。Java虚拟机提供了参数来控制 Java 堆和方法区的这两部分内存的最大值。
2GB(操作系统限制的内存大小)减去 Xmx(最大堆容量),再减去 MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。
所以每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。第一例中把栈空间占满而抛出 StackOverflowError 异常,第二例中把内存消耗完而抛出 OutOfMemoryError 异常。
方法栈溢出(从属于虚拟机栈的异常)
报错信息
java.lang.OutOfMemoryError : unable to create new native Thread
导致原因
出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。
解决方法
-
通过 -Xss 降低的每个线程栈大小的容量
-
线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制
/proc/sys/kernel/pid_max /proc/sys/kernel/thread-max max_user_process(ulimit -u) /proc/sys/vm/max_map_count
非常规溢出
下面这些OOM异常,可能大部分的同学都没有碰到过,但还是需要了解一下
分配超大数组
报错信息
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误。
解决方法就是检查你的代码中是否有创建超大数组的地方。
swap区溢出
报错信息 :
java.lang.OutOfMemoryError: Out of swap space
这种情况一般是操作系统导致的,可能的原因有:
- swap 分区大小分配不足;
- 其他进程消耗了所有的内存。
解决方案
- 其它服务进程可以选择性的拆分出去
- 加大swap分区大小,或者加大机器内存大小
本地方法溢出
报错信息 :
java.lang.OutOfMemoryError: stack_trace_with_native_method
本地方法在运行时出现了内存分配失败,和之前的方法栈溢出不同,方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在JNI代码或本地方法处。
本机直接内存溢出
-
直接内存可以通过:-XX:MaxDirectMemorySize 来设置大小,如果不设置,默认和堆在最大值-Xmx一样大。
-
设置本机直接内存的原则就是,各种内存大小+本机直接内存大小<机器物理内存。
下面程序利用 DirectByteBuffe 模拟直接内存溢出的情况
import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; public class DirectBufferOom { public static void main(String[] args) { final int _1M = 1024 * 1024; List<ByteBuffer> buffers = new ArrayList<>(); int count = 1; while (true) { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M); buffers.add(byteBuffer); System.out.println(count++); } } }
在命令行运行 java -XX:MaxDirectMemorySize=10M DirectBufferOom ,很快控制台就会出现异常
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:695) at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at DirectBufferOom.main(DirectBufferOom.java:12)
其实它并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常。下 面的程序利用 Unsafe 类模拟直接内存溢出
import sun.misc.Unsafe; import java.lang.reflect.Field; public class UnsafeOom { private static final int _1M = 1024 * 1024; public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException { Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1M); } } }
在命令行运行 java -XX:MaxDirectMemorySize=10M UnsafeOom ,结果如下
Exception in thread"main"java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)
由 DirectMemory 导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果读者发现 OOM 之后 Dump 文件很小,而程序中又直接或间接使用了 NIO ,那就可以考虑检查一下是不是这方面的原因。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
信息安全专家:威胁形势60年来最糟糕
在上周四的RSA主题演讲中,两名信息安全专家透露,与民族国家攻击活动相比,现在勒索软件对全球安全构成更大威胁。 Silverado Policy Accelerator公司董事长Dmitri Alperovitch和FireEye公司执行副总裁兼全球情报主管Sandra Joyce主持了有关当前全球威胁形势的讨论,在他们的讨论中,勒索软件攻击居首位。他们说,尽管民族国家团体利用全球疫情或攻击关键基础设施引起人们恐慌,并且DNS劫持的新趋势卷土重来,但他们表示勒索软件正在影响每个人。在过去几年中,随着勒索软件团伙转向勒索和双重勒索策略,这些风险不断提高。在主题演讲中,Alperovitch和Joyce透露,勒索软件的下一步发展可能更加危险。 Alperovitch说,总体而言,威胁环境正在变得前所未有的糟糕-不仅从技术角度来看,而且从地缘政治角度来看也是如此。他在会上说:“我们面对的对手包括-俄罗斯、伊朗和朝鲜等-从西方的角度来看,我们的关系是过去至少60年以来最糟糕的。” 信息安全专家谈到导致威胁形势变严峻的多种原因,其中大部分与战术和技术的简单演变有关。Joyce将网络称为“国家力量...
- 下一篇
Java技术专题-JVM研究系列(24)深入挖掘Java对象的内存结构
📕 每日一句 善于利用时间的人,总会拥有充分的时间。 📕 基本概念 在JVM虚拟机种Java对象的内存结构如图所示分为三大块:对象头(Object Header)、实例数据(Instance Data)、对齐填充(Padding)。 对象头:标记字段、类型指针、数组长度(限于数组对象)。 对象头中主要部分相关的数据大小: 对象头(Object header) Mark Word:对象的Mark Word部分占4个字节,其内容是一系列的标记位,比如轻量级的标记位(00),偏向锁标记位(01)等等。 Class对象指针:Class对象指针的大小也是4个字节,其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址。 对象实际数据:这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,refrence是4个字节。 对齐填充:最后一部分是对齐填充的字节,按8个字节填充 📕 对象头(Object Header) 📕 Mark W...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Linux系统CentOS6、CentOS7手动修改IP地址
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Windows10,CentOS7,CentOS8安装Nodejs环境