现象
- 所有的请求都卡住。
- 堆dump正常。
- 有一段时间内存占用高,GC频繁且耗时长,过了那段时间后监控上恢复正常。
- 日志有OutOfMemory的异常
结论
在这段代码OOM之前,它会导致JVM不停 fullGC 与 stopWorld,从而导致了程序卡死。(这也是为什么那一天的上午日志中并没有OOM,但是请求却也被卡住)
在这段OOM之后,之前由于它的不停的挤占空间,而它每次又只申请一小块内存,因此导致一些申请稍大内存的地方提前爆出如上的OOM异常。因此即使最后的问题代码OOM了,所占用的内存被回收掉了,但是由于一些重要线程挂了,依然会导致请求无法被处理。
如果线程的异常没有被捕获,那么JVM会在线程终结前打印日志,该日志会打印到标准流(service_stdout)中去,如下(这么多重要线程都挂了)
为什么操作系统显示进程占用内存很多,但是堆dump却只有600M的大小
原因
连续大片内存,回收时,JVM会还给操作系统,通过brk移动指针就可以办到。(malloc如果要向操作系统申请内存时,将brk指针向上移动,free如果要向操作系统归还内存时,将brk向下移动,这意味着,它必须归还最上面的连续内存)
但是如果是很多的小的内存碎片,JVM不会还给操作系统,因为通过移动brk指针不能办到。
如下代码,发现以前占用的小内存JVM没有还给操作系统,即使已经被垃圾回收了:
操作系统显示一直保持这个值不变,但是GC日志显示已经回收了。
测试代码:
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static volatile boolean shouldEnd = false;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
System.out.println("Loop Thread start");
List<LocalDate> array = new ArrayList<>();
while (!shouldEnd) {
LocalDate localDate = LocalDate.now();
array.add(localDate);
}
System.out.println("Loop Thread end");
} catch (Throwable e) {
System.out.println("Thread end");
e.printStackTrace();
}
}).start();
}
while (true) {
try {
if (!shouldEnd) {
Thread.sleep(10 * 1000);
} else {
Thread.sleep(1000);
}
byte[] arrays = new byte[512 * 1024 * 1024];
System.out.println("Clock get big area!");
} catch (Throwable e) {
shouldEnd = true;
e.printStackTrace();
}
}
}
}
收获
- 不要死循环
- try catch 的时候尽量去catch Throwable,而不是Exception,这样即使OutOfMemeoy的时候,也能捕获到这个异常。当然像上面这个问题,即使去申请一个稍微大一点的数据,也可能导致自己的线程OOM,我们的代码不可能每一行都在 try catch 范围中。