本节主要讲述相关的在日常开发过程中经常出现的内存溢出(OOM)问题,并对其进行分类和总结。
运行时数据堆溢出的主要原因是堆的内存不足,无法进行扩展或者分配更多的内存空间给Java对象。这种场景较为常见,其报错信息为:java.lang.OutOfMemoryError:Java heap space。
出现堆内存溢出的原因如下。
(1)内存空间本就无法支持业务场景,需要扩大内存空间。
(2)代码中可能存在大对象分配。
(3)可能存在内存泄漏,导致在多次GC以后,仍然无法找到一块足够的空间去容纳当前对象。
堆内存溢出解决方法主要有以下几种。
(1)如果没有找到明显的内存泄漏,则使用-Xms/-Xmx加大堆内存空间。
(2)检查是否存在大对象的内存分配问题,当存在数据量较大的数组或者集合的内存分配时,可以使用jmap这个命令,将整个堆中的内存数据dump下来,再利用mat内存分析工具解析一下,检查是不是可能存在内存泄漏的问题。
(3)还有一些重要问题容易被我们忽略,如是否有自定义的Finalizable对象,也有可能是从框架内部隐含实现的,有必要考虑它是否需要存在。
若线程中请求的栈的最大深度已经超过了虚拟机可容忍的最大栈深度,则线程就会立刻抛出StackOverflowError异常;如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemory-Error异常。
此处将异常分为两类情况比较严谨,但其中也存在着某些相互重复的地方:当栈空间无法再分配时,究竟是因为系统内存太小,还是因为已使用的栈空间太大?其本质就是对同一种事情的两种不同角度的描述。
通过-Xss参数,可以减小单个栈内存的容量,如果递归或者不断调用方法,会造成栈溢出StackOverflowError。相反,当-Xss栈内存越大时,可分配的线程数就相对越少或者输出的栈深度就越小,当无法申请到内存空间时,可能还未达到JVM的最大栈深度的阈值,就会提前抛出异常OutOfMemoryError。所以,结合以上两种情况,在分配栈内存的参数及编写方法调用链时深度需要合理得当。
在JDK 8之前版本,永久代是JVM对方法区的具体实现,其保存了被虚拟机自动加载的方法种类基本信息、常数、静态值和变量,以及所有JIT编译器编译之后的运行代码等。
在JDK 8之后版本,元空间替换了永久代。元空间使用的是本地内存,字符串常量由永久代转移到堆中,和永久代相关的JVM参数已移除。
永久代及元空间内存溢出所产生的报错信息分别如下:java.lang.OutOfMemoryError: PermGen space和java.lang.OutOfMemoryError: Metaspace。
永久代和元空间内存溢出的原因可能有如下几种。
(1)在Java 7之前,频繁地错误使用String.intern方法。
(2)生成大量的代理类,导致方法区被撑爆,无法卸载。
(3)应用长时间运行,没有重启。
JVM是运行在操作系统上的进程,操作系统在JVM启动时分配给它的内存是有限的,不可能把全部内存都分配给JVM。NIO便用了直接内存技术,利用Channel和Buffer直接操作JVM外的内存,避免数据在JVM和操作系统内存之间来回复制。直接内存可以通过-XX:MaxDirectMemorySize进行设置,如果不设置,则默认和堆的最大值-Xmx一样大。
设置本机直接内存的原则:各种内存大小+本机直接内存大小<机器物理内存。当堆内存和直接内存的和大于操作系统总内存时,就会发生内存溢出异常OutOfMemoryError。
执行后的测试结果如下:
Unsafe实现对直接内存的分配与回收:
综上可以看出,采用Full GC可以分配内存,采用Unsafe.freeMemory方法可以清理内存。