每日一句
只有经历地狱般的磨练,才能创造出天堂般的力量。
堆(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虚拟机规范中描述了两种异常:
-
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常。
-
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里把异常分成两种情况看似更加严谨,但却存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
虚拟机的 *Error 异常
-Xss参数减小栈内存的容量,然后不断调用方法造成栈溢出,*Error 异常。
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.*Error
at test_JVMStackSOF.JVMStackSOF.stackPush(JVMStackSOF.java:17)
at test_JVMStackSOF.JVMStackSOF.stackPush(JVMStackSOF.java:18)
-Xss256K:设置参数栈内存容量为256K
- 在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是*Error异常。
使用-Xss参数减少栈内存容量。结果:抛出*Error异常,异常出现时输出的栈深度相应缩小。
定义了大量的本地变量,增加此方法帧中本地变量表的长度。结果:抛出*Error异常时输出的栈深度相应缩小。
虚拟机栈隔离的,每个线程都有自己独立的虚拟机栈。
在 Java 虚拟机规范中,对虚拟机栈这个区域规定了两种异常状况:
-
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 *Error 异常;
-
如果虚拟机栈可以动态扩展(当前大部分的 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(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。
所以每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。第一例中把栈空间占满而抛出 *Error 异常,第二例中把内存消耗完而抛出 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 ,那就可以考虑检查一下是不是这方面的原因。