Java之OOM

java.lang.StackOverflowError

栈溢出错误,这个错误很容易模拟,且看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
new StackOverflowTest().test();
}

private static int high = 0;

private void test() {
try {
++high;
test();
} finally {
System.out.println("栈的深度为: " + high);
}
}

---
JVM ARGS: -server -Xmn2m -Xss1m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -XX:-UseTLAB

我们都知道方法的调用是通过入栈和计算出栈来实现的,所以我们在方法递归调用一定次数时,必然会发生栈溢出,栈溢出后,程序自动停止,是一类不可捕获和恢复的Error类型的错误,所以我们在使用递归算法时,应当注意递归的深度,防止出现栈溢出错误导致服务错误

java.lang.OutOfMemoryError:java heap space

JVM堆空间不足引起的内存溢出错误,这类错误比较常见,此处就不做太多的解释,出现这类错误,你就去看GC日志,看看新生代、老年代、永久代/Metaspace的使用情况,如果是想查看GC的情况,使用如下JVM指令:

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:./gclog.log,gc的所有信息都会输出到gclog.log文件中

gclog.log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
*JVM信息*
Java HotSpot(TM) 64-Bit Server VM (25.161-b12) for bsd-amd64 JRE (1.8.0_161-b12), built on Dec 19 2017 16:22:20 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 16777216k(2991720k free)

/proc/meminfo:

*JVM ARGS*
CommandLine flags: -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=2097152 -XX:NewSize=2097152 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:ThreadStackSize=1024 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC -XX:-UseTLAB

*GC日志信息*
0.125: [GC (Allocation Failure) [PSYoungGen: 1023K->512K(1536K)] 1023K->536K(261632K), 0.0010704 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
0.157: [GC (Allocation Failure) [PSYoungGen: 1535K->493K(1536K)] 1559K->847K(261632K), 0.0010655 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
...
0.360: [GC (Allocation Failure) [PSYoungGen: 1247K->256K(1536K)] 2614K->1727K(261632K), 0.0008285 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Heap
*年轻代*
PSYoungGen total 1536K, used 396K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1024K, 13% used [0x00000007bfe00000,0x00000007bfe23268,0x00000007bff00000)
from space 512K, 50% used [0x00000007bff00000,0x00000007bff40000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
*老年代*
ParOldGen total 260096K, used 1471K [0x00000006c0000000, 0x00000006cfe00000, 0x00000007bfe00000)
object space 260096K, 0% used [0x00000006c0000000,0x00000006c016fc00,0x00000006cfe00000)
*Metaspace空间,jdk8+*
Metaspace used 3402K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 368K, capacity 388K, committed 512K, reserved 1048576K

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
new StackOverflowTest().heapSpace();
}

private void heapSpace() {
List<String> list = new ArrayList<>();
while (true) {
list.add(new String("abc"));
}
}

---
JVM ARGS: -server -Xmn2m -Xss1m -Xms1m -Xmx1m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:-DoEscapeAnalysis -XX:-EliminateAllocations -XX:-UseTLAB

java.lang.OutOfMemoryError:GC overhead limit exceeded

超出了GC开销限制引起的内存溢出,这个错误不是特别常见,Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常,可以使用参数-XX:-UseGCOverheadLimit 禁用这个检查,但是这个参数解决不了内存问题,只是把错误的信息延后,替换成 java.lang.OutOfMemoryError: Java heap space

java.lang.OutOfMemoryError:Metaspace

Metaspace内存溢出,Metaspace是jdk8+特有的东西,用来代替之前的PermGen,主要存储class名称、字段、方法、字节码、常量池、JIT优化代码等等,我们可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定其大小,一般情况下Metaspace不会发生OOM,Metaspace的使用量与JVM加载的class数量有很大关系:

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static ClassPool cp = ClassPool.getDefault();

public static void main(String[] args) throws CannotCompileException {
int i = 0;
try {
for (;; i++) {
Class cz = cp.makeClass("com.example.demo.bean.DemoBean" + i).toClass();
}
} catch (Exception e) {
} finally {
System.out.println(i);
}
}

---
JVM ARGS: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0.598: [GC (Metadata GC Threshold) [PSYoungGen: 39345K->10741K(76288K)] 39345K->15811K(251392K), 0.0111319 secs] [Times: user=0.05 sys=0.01, real=0.01 secs] 
0.609: [Full GC (Metadata GC Threshold) [PSYoungGen: 10741K->0K(76288K)] [ParOldGen: 5069K->15550K(139776K)] 15811K->15550K(216064K), [Metaspace: 9735K->9735K(1056768K)], 0.0504762 secs] [Times: user=0.29 sys=0.01, real=0.05 secs]
...
0.754: [GC (Last ditch collection) [PSYoungGen: 0K->0K(82944K)] 15477K->15477K(472064K), 0.0008113 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
0.755: [Full GC (Last ditch collection) [PSYoungGen: 0K->0K(82944K)] [ParOldGen: 15477K->15477K(607232K)] 15477K->15477K(690176K), [Metaspace: 9733K->9733K(1056768K)], 0.0204189 secs] [Times: user=0.08 sys=0.00, real=0.02 secs]
5341
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at javassist.ClassPool.toClass(ClassPool.java:1170)
at javassist.ClassPool.toClass(ClassPool.java:1113)
at javassist.ClassPool.toClass(ClassPool.java:1071)
at javassist.CtClass.toClass(CtClass.java:1275)
at com.example.demo.jvm.MetaspceOOMTest.main(MetaspceOOMTest.java:13)
Heap
PSYoungGen total 82944K, used 2390K [0x000000076ab00000, 0x0000000772c00000, 0x00000007c0000000)
eden space 82432K, 2% used [0x000000076ab00000,0x000000076ad55ab0,0x000000076fb80000)
from space 512K, 0% used [0x0000000772b80000,0x0000000772b80000,0x0000000772c00000)
to space 10752K, 0% used [0x0000000771700000,0x0000000771700000,0x0000000772180000)
ParOldGen total 607232K, used 15477K [0x00000006c0000000, 0x00000006e5100000, 0x000000076ab00000)
object space 607232K, 2% used [0x00000006c0000000,0x00000006c0f1d4c8,0x00000006e5100000)
Metaspace used 9770K, capacity 10084K, committed 10240K, reserved 1056768K
class space used 3165K, capacity 3214K, committed 3328K, reserved 1048576K

我们将Metaspace的初始大小和最大值都设置为10m,最终i的值大概会在5340左右的时候报OOM,从FGC的日志可以看出,Metaspace在整个GC阶段都未进行任务的内存回收,直至被全部用完,具体的关于Metaspace的介绍可以看下PerfMa社区的这篇文章:https://club.perfma.com/article/210111

java.lang.OutOfMemoryError:Direct buffer memory

ByteBuffer. allocateDirect (int capability)是分配操作系统的本地内存,不在GC管辖范围之内,由于不需要内存拷贝所以速度相对较快,但如果不断分配本地内存,堆内存就会很少使用,那么JVM就不需要进行GC,那创建的DirectByteBuffer对象就不会被回收,就会出现堆内存充足但本地内存不足的情况,继续尝试分配本地内存就会出现OOM。

代码

1
2
3
4
5
6
7
public static void main(String[] args) {
System.out.println("当前direct大小: " + (VM.maxDirectMemory() / 1024 / 1024) + " MB");
ByteBuffer bb = ByteBuffer.allocateDirect(Math.toIntExact(VM.maxDirectMemory() + 10));
}

---
JVM ARGS: -XX:MaxDirectMemorySize=10m

这里我们需要通过JVM参数-XX:MaxDirectMemorySize=10将JVM本地最大使用内存设置为10MB,不然如果你本地剩余内存很大,那么就很难模拟出此错误

输出

1
2
3
4
5
6
当前direct大小: 10 MB
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.example.demo.jvm.DirectBufferOOMTest.main(DirectBufferOOMTest.java:11)

java.lang.OutOfMemoryError:unable create new native thread

线程创建的太多,导致无法继续创建线程,出现这个问题就要去使用jstack导出线程栈查看具体情况

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
while (true) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
}
}
}).start();
}
}

这一段代码必然会出现该ERROR,不论你的机器有多牛掰,你会发现出现了OOM之后,进程并未终止,这个时候你可以用jps命令查看进程号,然后使用jstack pid查看线程栈,会发现有非常多的线程处于TIMED_WAITING (sleeping)状态:

1
2
3
4
5
"Thread-256" #267 prio=5 os_prio=31 tid=0x00007fccdd8cc000 nid=0x27d03 waiting on condition [0x0000700019b85000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.example.demo.jvm.NativeThreadOOMTest$1.run(NativeThreadOOMTest.java:11)
at java.lang.Thread.run(Thread.java:748)