Java内存溢出(OOM)异常排查指南
这也许是⽬前最为完整的Java OOM异常的解决指南。
1、java.lang.OutOfMemoryError:Java heap space
Java应⽤程序在启动时会指定所需要的内存⼤⼩,它被分割成两个不同的区域:Heap space(堆空间)和Permgen(永久代):
JVM内存模型⽰意图
这两个区域的⼤⼩可以在JVM(Java虚拟机)启动时通过参数-Xmx和-XX:MaxPermSize设置,如果你没有显式设置,则将使⽤特定平台的默认值。
当应⽤程序试图向堆空间添加更多的数据,但堆却没有⾜够的空间来容纳这些数据时,将会触发java.lang.OutOfMemoryError: Java heap space异常。需要注意的是:即使有⾜够的物理内存可⽤,只要达到堆空间设置的⼤⼩限制,此异常仍然会被触发。
原因分析
触发java.lang.OutOfMemoryError: Java heap space最常见的原因就是应⽤程序需要的堆空间是XXL号的,但是JVM提供的却是S号。解决⽅法也很简单,提供更⼤的堆空间即可。除了前⾯的因素还有更复杂的成因:
流量/数据量峰值:应⽤程序在设计之初均有⽤户量和数据量的限制,某⼀时刻,当⽤户数量或数据量突然达到⼀个峰值,并且这个峰值已经超过了设计之初预期的阈值,那么以前正常的功能将会停⽌,并触发java.lang.OutOfMemoryError: Java heap space异常。
内存泄漏:特定的编程错误会导致你的应⽤程序不停的消耗更多的内存,每次使⽤有内存泄漏风险的功能就会留下⼀些不能被回收的对象到堆空间中,随着时间的推移,泄漏的对象会消耗所有的堆空间,最终触发java.lang.OutOfMemoryError: Java heap space错误。
⽰例
①、简单⽰例
⾸先看⼀个⾮常简单的⽰例,下⾯的代码试图创建2 x 1024 x 1024个元素的整型数组,当你尝试编译并指定12M堆空间运⾏时(java -Xmx12m OOM)将会失败并抛出java.lang.OutOfMemoryError: Java heap space错误,⽽当你指定13M堆空间时,将正常的运⾏。
计算数组占⽤内存⼤⼩,不再本⽂的范围内,读者有兴趣,可以⾃⾏计算
class OOM {
static final int SIZE=2*1024*1024;
public static void main(String[] a) {
int[] i = new int[SIZE];
}
}
运⾏如下:
D:\>javac OOM.java
D:\>java -Xmx12m OOM
Exception in thread "main"java.lang.OutOfMemoryError: Java heap space
at OOM.main(OOM.java:4)
D:\>java -Xmx13m OOM
②、内存泄漏⽰例
在Java中,当开发者创建⼀个新对象(⽐如:new Integer(5))时,不需要⾃⼰开辟内存空间,⽽是把它交给JVM。在应⽤程序整个⽣命周期类,JVM负责检查哪些对象可⽤,哪些对象未被使⽤。未使⽤对象将被丢弃,其占⽤的内存也将被回收,这⼀过程被称为垃圾回收。JVM负责垃圾回收的模块集合被称为垃圾回收器(GC)。
Java的内存⾃动管理机制依赖于GC定期查找未使⽤对象并删除它们。Java中的内存泄漏是由于GC⽆法识别⼀些已经不再使⽤的对象,⽽这些未使⽤的对象⼀直留在堆空间中,这种堆积最终会导致java.lang.OutOfMemoryError: Java heap space错误。
我们可以⾮常容易的写出导致内存泄漏的Java代码:
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map<Key,String> m = new HashMap<Key,String>();
while(true) {
for(int i=0;i<10000;i++) {
if(!m.containsKey(new Key(i))) {
m.put(new Key(i), "Number:" + i);
}
}
}
}
}
代码中HashMap为本地缓存,第⼀次while循环,会将10000个元素添加到缓存中。后⾯的while循环中,由于key已经存在于缓存中,缓存的⼤⼩将⼀直会维持在10000。但事实真的如此吗?由于Key实体没有实现equals()⽅法,导致for循环中每次执⾏m.containsKey(new Key(i))结果均为fal,其结果
就是HashMap中的元素将⼀直增加。
随着时间的推移,越来越多的Key对象进⼊堆空间且不能被垃圾收集器回收(m为局部变量,GC会认为这些对象⼀直可⽤,所以不会回收),直到所有的堆空间被占⽤,最后抛出java.lang.OutOfMemoryError:Java heap space。
上⾯的代码直接运⾏可能很久也不会抛出异常,可以在启动时使⽤-Xmx参数,设置堆内存⼤⼩,或者在for循环后打印HashMap的⼤⼩,执⾏后会发现HashMap的size⼀直再增长。
解决⽅法也⾮常简单,只要Key实现⾃⼰的equals⽅法即可:
Override
public boolean equals(Object o) {
boolean respon = fal;
if (o instanceof Key) {
respon = (((Key)o).id).equals(this.id);
}
return respon;
}
解决⽅案
第⼀个解决⽅案是显⽽易见的,你应该确保有⾜够的堆空间来正常运⾏你的应⽤程序,在JVM的启动配置中增加如下配置:
-Xmx1024m
上⾯的配置分配1024M堆空间给你的应⽤程序,当然你也可以使⽤其他单位,⽐如⽤G表⽰GB,K表⽰KB。下⾯的⽰例都表⽰最⼤堆空间为1GB:
java -pany.MyClass
java -pany.MyClass
java -pany.MyClass
java -pany.MyClass
然后,更多的时候,单纯地增加堆空间不能解决所有的问题。如果你的程序存在内存泄漏,⼀味的增加堆空间也只是推
迟java.lang.OutOfMemoryError: Java heap 又可以组什么词
space错误出现的时间⽽已,并未解决这个隐患。除此之外,垃圾收集器在GC时,应⽤程序会停⽌运⾏直到GC完成,⽽增加堆空间也会导致GC时间延长,进⽽影响程序的吞吐量。
如果你想完全解决这个问题,那就好好提升⾃⼰的编程技能吧,当然运⽤好Debuggers, profilers, heap dump analyzers等⼯具,可以让你的程序最⼤程度的避免内存泄漏问题。
2、java.lang.OutOfMemoryError:GC overhead limit exceeded
Java运⾏时环境(JRE)包含⼀个内置的垃圾回收进程,⽽在许多其他的编程语⾔中,开发者需要⼿动分配和释放内存。
Java应⽤程序只需要开发者分配内存,每当在内存中特定的空间不再使⽤时,⼀个单独的垃圾收集进程会清空这些内存空间。垃圾收集器怎样检测内存中的某些空间不再使⽤已经超出本⽂的范围,但你只需要相信GC可以做好这些⼯作即可。
默认情况下,当应⽤程序花费超过98%的时间⽤来做GC并且回收了不到2%的堆内存时,会抛出java.lang.OutOfMemoryError:GC overhead limit exceeded错误。具体的表现就是你的应⽤⼏乎耗尽所有可⽤内存,并且GC多次均未能清理⼲净。
原因分析
java.lang.OutOfMemoryError:GC overhead limit exceeded错误是⼀个信号,⽰意你的应⽤程序在垃圾收集上花费了太多时间但却没有什么卵⽤。默认超过98%的时间⽤来做GC却回收了不到2%的内存时将会抛出此错误。那如果没有此限制会发⽣什么呢?GC进程将被重
启,100%的CPU将⽤于GC,⽽没有CPU资源⽤于其他正常的⼯作。如果⼀个⼯作本来只需要⼏毫秒即可完成,现在却需要⼏分钟才能完成,我想这种结果谁都没有办法接受。
所以java.lang.OutOfMemoryError:GC鱿鱼炒饭的做法
overhead limit exceeded也可以看做是⼀个fail-fast(快速失败)实战的实例。
⽰例
下⾯的代码初始化⼀个map并在⽆限循环中不停的添加键值对,运⾏后将会抛出GC overhead limit exceeded错误:
public class Wrapper {
public static void main(String args[]) throws Exception {
Map map = Properties();
Random r = new Random();
while (true) {
map.Int(), "value");
}
}
}
正如你所预料的那样,程序不能正常的结束,事实上,当我们使⽤如下参数启动程序时:
java -Xmx100m -XX:+UParallelGC Wrapper
我们很快就可以看到程序抛出java.lang.OutOfMemoryError: GC overhead limit exceeded错误。但如果在启动时设置不同的堆空间⼤⼩或者使⽤不同的GC算法,⽐如这样:
java -Xmx10m -XX诚信的经典句子
:+UPa邱二娘
rallelGC Wrapper
我们将看到如下错误:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap庄心妍
space
at java.hash(Unknown Source)
at java.util.Hashtable.addEntry(Unknown Source)
at java.util.Hashtable.put(Unknown Source)
dev.Wrapper.main(Wrapper.java:12)
使⽤以下GC算法:-XX:+UConcMarkSweepGC 或者-XX:+UG1GC,启动命令如下:
java -Xmx100m -XX:+UConcMarkSweepGC Wrapper
java -Xmx100m -XX:+UG1GC Wrapper
得到的结果是这样的:
Exception: java.lang.OutOfMemoryError thrown from
the UncaughtExceptionHandler in thread "main"
错误已经被学生自荐信
默认的异常处理程序捕获,并且没有任何错误的堆栈信息输出。
以上这些变化可以说明,在资源有限的情况下,你根本⽆法⽆法预测你的应⽤是怎样挂掉的,什么时候会挂掉,所以在开发时,你不能仅仅保证⾃⼰的应⽤程序在特定的环境下正常运⾏。
解决⽅案
⾸先是⼀个毫⽆诚意的解决⽅案,如果你仅仅是不想看到java.lang.OutOfMemoryError:GC overhead limit exceeded的错误信息,可以在应⽤程序启动时添加如下JVM参数:
-XX:-UGCOverheadLimit
但是强烈建议不要使⽤这个选项,因为这样并没有解决任何问题,只是推迟了错误出现的时间,错误信息也变成了我们更熟悉
的java.lang.OutOfMemoryError: Java heap space⽽已。
另⼀个解决⽅案,如果你的应⽤程序确实内存不⾜,增加堆内存会解决GC overhead limit问题,就如下⾯这样,给你的应⽤程序1G的堆内存:
java -urcompany.YourClass
但如果你想确保你已经解决了潜在的问题,⽽不是掩盖java.lang.OutOfMemoryError: GC overhead limit exceeded错误,那么你不应该仅⽌步于此。你要记得还有profilers和memory dump analyzers这些⼯具,你需要花费更多的时间和精⼒来查找问题。还有⼀点需要注意,这些⼯具在Java运⾏时有显著的开销,因此不建议在⽣产环境中使⽤。
3、java.lang.OutOfMemoryError:Permgen space
Java中堆空间是JVM管理的最⼤⼀块内存空间,可以在JVM启动时指定堆空间的⼤⼩,其中堆被划分成两个不同的区域:新⽣代(Young)和⽼年代(Tenured),新⽣代⼜被划分为3个区域:Eden、From Survivor、To Survivor,如下图所⽰。
图⽚来源:并发编程⽹
java.lang.OutOfMemoryError: PermGen space错误就表明持久代所在区域的内存已被耗尽。
原因分析
要理解java.lang.OutOfMemoryError: PermGen space出现的原因,⾸先需要理解Permanent Generation Space的⽤处是什么。持久代主要存储的是每个类的信息,⽐如:类加载器引⽤、运⾏时常世界上最大的海龟
量池(所有常量、字段引⽤、⽅法引⽤、属性)、字段(Field)数据、⽅法(Method)数据、⽅法代码、⽅法字节码等等。我们可以推断出,PermGen的⼤⼩取决于被加载类的数量以及类的⼤⼩。
因此,我们可以得出出现java.lang.OutOfMemoryError: PermGen space错误的原因是:太多的类或者太⼤的类被加载到permanent generation(持久代)。
⽰例
①、最简单的⽰例
正如前⾯所描述的,PermGen的使⽤与加载到JVM类的数量有密切关系,下⾯是⼀个最简单的⽰例:
import javassist.ClassPool;
public class MicroGenerator {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100_000_000; i++) {
generate("cn.moondev.Ur" + i);
}
}
public static Class generate(String name) throws Exception {
ClassPool pool = Default();
return pool.makeClass(name).toClass();
}
}
运⾏时请设置JVM参数:-XX:MaxPermSize=5m,值越⼩越好。需要注意的是JDK8已经完全移除持久代空间,取⽽代之的是元空间(Metaspace),所以⽰例最好的JDK1.7或者1.6下运⾏。
代码在运⾏时不停的⽣成类并加载到持久代中,直到撑满持久代内存空间,最后抛出java.lang.OutOfMemoryError:Permgen space。代码中类的⽣成使⽤了javassist库。
②、Redeploy-time