【JVM】TroubleShooting之内存溢出异常(OOM)与调优

时间:2021-11-15 22:37:26

1. OOM概述

If your application's execution time becomes longer and longer, or if the operating system seems to be performing slower and slower, this could be an indication of a memory leak. In other words, virtual memory is being allocated but is not being returned when it is no longer needed. Eventually the application or the system runs out of memory, and the application terminates abnormally.

如果您的应用程序的执行时间变得越来越长,或者操作系统看起来速度越来越慢,则这可能表示内存泄漏。换句话说,虚拟内存正在分配,但不再需要时不会返回。最终应用程序或系统内存不足,应用程序异常终止。

以上摘自《Troubleshooting Guide for HotSpot VM》

------------------------------

2. OOM异常

2.1 常见原因

内存泄漏常见的异常提示是java.lang.OutOfMemoryError。产生这个错误通常是出现以下几种情况:

(1) 程序开启过多的线程

(2) 配置参数值设置过小,不满足程序的资源要求

(3) 集合对象(Collection、Map)始终存有对象的引用,使用完后未及时清除,未及时被GC回收。

(4) 一次性从数据库中获取大量的数据量,造成内存加载的数据量过于庞大

(5) 代码中存在死循环,大量出现重复的新对象体

(6) 引用的第三方类库中有BUG

2.2 常见的异常提示

 (1). Exception in thread "main": java.lang.OutOfMemoryError: Java heap space

这个异常信息表示无法在Java 堆中分配对象。比如以下这段代码

/**
* @author Supernova
* @date 2018/06/18
*/
public class HeapOOM{
private static StringBuffer name;
public static void main(String[] args){
name = new StringBuffer("Supernova");
for(int i = 0; i < Integer.MAX_VALUE; i++){
name = name.append(name);
}
}
}

 运行情况如下:抛出了OOM异常提示Java heap space。

【JVM】TroubleShooting之内存溢出异常(OOM)与调优

当StringBuffer对象超出了容量限制,就无法在堆中分配内存。同样,如果程序中创建了过多的对象也会因对象数量超出容量限制而出现OOM。

 (2). Exception in thread "main": java.lang.OutOfMemoryError: PermGen space

异常信息提示持久代空间已满,它可能出现在加载很多类的时候,比如引用了很多第三方库。持久代存储的是类、方法等信息。持久代在在JVM中保留至JDK1.7,在JDK1.8之后被删除,详情请看上篇博文【JVM】上帝视角看JVM内存模型,分而治之论各模块详情。JDK1.6时,静态变量存储在持久栈中,JDK1.7之后,静态变量存储在堆中。这里我们通过讨论OOM异常证明静态变量的存储位置。

如以下这段可使静态变量造成内存溢出的代码:

import java.util.ArrayList;  

/**
* @author Supernova
* @date 2018/06/18
*/
public class StaticOOM{
private static String name = "Supernova";
private static ArrayList<String> list ;
public static void main(String[] args){
list = new ArrayList<String>();
for(int i = 0; i < Integer.MAX_VALUE; i++){
name = name + name;
list.add(name.intern());
}
}
}

JDK1.6下运行情况:抛出的异常提示PermGen space

【JVM】TroubleShooting之内存溢出异常(OOM)与调优

JDK1.7下运行情况:抛出的异常提示Java heap space

【JVM】TroubleShooting之内存溢出异常(OOM)与调优

JDK1.8下运行情况:抛出的异常提示Java heap space

【JVM】TroubleShooting之内存溢出异常(OOM)与调优

因此,可以得出结论,静态变量在JDK1.6是存放在方法区的持久代中,在JDK1.7之后存放在Java堆中

 (3). Exception in thread "main": java.lang.OutOfMemoryError: Metaspace

在JDK1.7之后,元空间(meta space)取代了持久代,所以在JDK1.7之后,在程序中也会看到元空间的异常信息。通常是由于加载太多的类,超出了限制值。

如以下代码:

package com.nova.test;
/**
* @author Supernova
* @date 2018/06/18
*/
public class MetaSpaceOOM {
public static void main(String[] args) {
javassist.ClassPool jc = new javassist.ClassPool().getDefault();
for(int i = 1 ;i < Integer.MAX_VALUE; i++) {
try {
Class myClass = jc.makeClass("SupernovaClass"+i).toClass();
System.out.println("加载第"+i+"个类");
}catch(Exception e){
e.printStackTrace();
}
}
}
}

为了加快溢出,在Eclipse中设置元空间的最大值为20M。

【JVM】TroubleShooting之内存溢出异常(OOM)与调优

运行结果如下:

当加载类的数量达到17261个类的时候,抛出了元空间OOM异常。也就是在20m的限制值之下,只能加载17261个类,一旦超出,20m的限制值就会产生OOM,所以在具体项目中,应结合实际情况设置-XX:MaxMetaspaceSize参数来解决或防止OOM的异常。

【JVM】TroubleShooting之内存溢出异常(OOM)与调优

 (3). Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit

异常信息表示是由于数组大小超过VM限制。如以下情况,

/**
* @author Supernova
* @date 2018/06/18
*/
public class ArrayOOM{
public static void main(String[] args){
int[] arr = new int[Integer.MAX_VALUE];
}
}

代码中请求创建的是Int类型的数组,int长度为2^31 - 1,多数平台的限制都大约是这个值。

运行结果如下:

【JVM】TroubleShooting之内存溢出异常(OOM)与调优

 (4). Exception in thread "main": java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

本机方法分配故障。它是在JNI或者是本地方法中检测到的。如果遇到这个异常,可能需要用到操作系统特定工具(Operation-System-Specific Tools)才能排障,

详情可参考:https://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/tooldescr.html#gbmoy

 (5). Exception in thread "main": java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)

这类异常通常是本地堆分配失败造成应用程序崩溃。这种异常的排障比较难,需要根据具体情况,出现这类通常需要从转储日志或者致命错误日志进行诊断。

致命错误日志可参考:https://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/felog.html

3. 调优

 3.1 代码调优:

(1) 减少使用临时对象,对象不使用时候,最好显示的设置为null。

(2) 不过多的使用静态对象,因为static对象属于全局变量,会一直占用内存,不被回收

(3) 累加字符串尽量使用StringBuffer,而不用String,因为StringBuffer是可变字符串,累加操作是在一个对象里,而String是固定长度的,累加操作时不是在一个String对象里,而是创建新的对象

3.2 参数调优:

--Xmx: 设置JVM堆的最大值

--Xms: 设置JVM堆最小值

--Xss: 设置每个线程栈的大小

--Xmn: 设置年轻代大小

--XX:MaxPermGenSize: 设置持久代大小

--XX:MaxTenuringThreshold: 设置年龄阈值

更多的参数调优参考下表,此表引用于:JVM系列三:JVM参数设置、分析

参数名称 含义 默认值  
-Xms 初始堆大小 物理内存的1/64(<1GB) 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
-Xmx 最大堆大小 物理内存的1/4(<1GB) 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-Xmn 年轻代大小(1.4or lator)   注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.
增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-XX:NewSize 设置年轻代大小(for 1.3/1.4)    
-XX:MaxNewSize 年轻代最大值(for 1.3/1.4)    
-XX:PermSize 设置持久代(perm gen)初始值 物理内存的1/64  
-XX:MaxPermSize 设置持久代最大值 物理内存的1/4  
-Xss 每个线程的堆栈大小   JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)
和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"”
-Xss is translated in a VM flag named ThreadStackSize”
一般设置这个值就可以了。
-XX:ThreadStackSize Thread Stack Size   (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]
-XX:NewRatio 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)   -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
-XX:SurvivorRatio Eden区与Survivor区的大小比值   设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
-XX:LargePageSizeInBytes 内存页的大小不可设置过大, 会影响Perm的大小   =128m
-XX:+UseFastAccessorMethods 原始类型的快速优化    
-XX:+DisableExplicitGC 关闭System.gc()   这个参数需要严格的测试
-XX:MaxTenuringThreshold 垃圾最大年龄   如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
该参数只有在串行GC时才有效.
-XX:+AggressiveOpts 加快编译    
-XX:+UseBiasedLocking 锁机制的性能改善    
-Xnoclassgc 禁用垃圾回收    
-XX:SoftRefLRUPolicyMSPerMB 每兆堆空闲空间中SoftReference的存活时间 1s softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap
-XX:PretenureSizeThreshold 对象超过多大是直接在旧生代分配 0 单位字节 新生代采用Parallel Scavenge GC时无效
另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.
-XX:TLABWasteTargetPercent TLAB占eden区的百分比 1%  
-XX:+CollectGen0First FullGC时是否先YGC false  

参考

https://blog.csdn.net/renfufei/article/details/78170188?locationNum=2&fps=1

https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html

https://blog.csdn.net/wys839348916/article/details/46312523