乐趣区

关于JVM内存溢出的原因分析及解决方案探讨

前言:JVM 中除了程序计数器,其他的区域都有可能会发生内存溢出。

0. 什么是内存溢出

当程序需要申请内存的时候,由于没有足够的内存,此时就会抛出 OutOfMemoryError,这就是内存溢出。

1. 内存泄漏和内存溢出区别与联系

  • 内存泄漏:系统分配的内存没有被回收。
  • 内存溢出:分配的内存空间超过系统内存。

2. 内存泄漏的原因分析

jvm 由 5 大块组成:堆,栈,本地方法栈,程序计数器,方法区。栈它的主要记录方法的执行和对象的引用。堆则存在真正的引用的对象。

内存泄漏是由于使用不当,把一部分内存“丢掉了”,导致这部分内存不可用。

当在堆中创建了对象,后来没有使用这个对象了,又没有把整个对象的相关引用设为 null。此时垃圾收集器会认为这个对象是需要的,就不会清理这部分内存。这就会导致这部分内存不可用。

所以内存泄漏会导致可用的内存减少,进而会导致内存溢出。

3. JVM 垃圾回收机制思想

就是从栈出发(root),遍历对象的引用,在遍历堆里面的引用对象,因为栈中的对象的引用执行完毕就删除,所以我们就可以通过栈中的对象的引用,查找到堆中没有被指向的对象,这些对象即为不可到达对象,对其进行垃圾回收。

4. 内存溢出的原因分析

内存溢出是由于没被引用的对象(垃圾)过多造成 JVM 没有及时回收,造成的内存溢出。如果出现这种现象可行代码排查:

  1. 是否 App 中的类中和引用变量过多使用了 Static 修饰 如 public staitc Student s;在类中的属性中使用
    static 修饰的最好只用基本类型或字符串。如 public static int i = 0; //public static
    String str;
  2. 是否 App 中使用了大量的递归或无限递归(递归中用到了大量的建新的对象)
  3. 是否 App 中使用了大量循环或死循环(循环中用到了大量的新建的对象)
  4. 检查 App 中是否使用了向数据库查询所有记录的方法。即一次性全部查询的方法,如果数据量超过 10 万多条了,就可能会造成内存溢出。所以在查询时应采用“分页查询”。
  5. 检查是否有数组,List,Map 中存放的是对象的引用而不是对象,因为这些引用会让对应的对象不能被释放。会大量存储在内存中。
  6. 检查是否使用了“非字面量字符串进行 +”的操作。因为 String 类的内容是不可变的,每次运行 ”+” 就会产生新的对象,如果过多会造成新 String 对象过多,从而导致 JVM 没有及时回收而出现内存溢出。

如:

String s1 = "My name";
String s2 = "is";
String s3 = "xiaomanong";
String str = s1 + s2 + s3 +.........;

这是会容易造成内存溢出的

但是 String str = “My name” + ” is ” + ” xuwei” + ” nice ” + ” to ” + ” meet you”; // 但是这种就不会造成内存溢出。因为这是”字面量字符串“,在运行 ”+” 时就会在编译期间运行好。不会按照 JVM 来执行的。

在使用 String,StringBuffer,StringBuilder 时,如果是字面量字符串进行 ”+” 时,应选用 String 性能更好;如果是 String 类进行 ”+” 时,在不考虑线程安全时,应选用 StringBuilder 性能更好。

5. 常见的四种内存溢出情况

  1. 堆溢出 (OutOfMemoryError:java heap space)
  2. 持久代溢出 (OutOfMemoryError: PermGen space)
  3. 栈溢出 (StackOverflowError)
  4. OutOfMemoryError:unable to create native thread

1)堆溢出:JVM Heap:java.lang.OutOfMemoryError: Java heap space

JVM 在启动的时候会自动设置 JVM Heap 的值,可以利用 JVM 提供的 -Xmn -Xms -Xmx 等选项可进行设置。Heap 的大小是 Young Generation 和 Tenured Generaion 之和。在 JVM 中如果 98% 的时间是用于 GC, 且可用的 Heap size 不足 2% 的时候将抛出此异常信息。

解决方法 :手动设置 JVM Heap(堆)的大小。

2)持久代溢出:PermGen space:java.lang.OutOfMemoryError: PermGen space

PermGen space 的全称是 Permanent Generation space, 是指内存的永久保存区域。为什么会内存溢出,这是由于这块内存主要是被 JVM 存放 Class 和 Meta 信息的,Class 在被 Load 的时候被放入 PermGen space 区域,它和存放 Instance 的 Heap 区域不同,sun 的 GC 不会在主程序运行期对 PermGen space 进行清理,所以如果你的 APP 会载入很多 CLASS 的话,就很可能出现 PermGen space 溢出。一般发生在程序的启动阶段。

解决方法 :通过 -XX:PermSize 和 -XX:MaxPermSize 设置永久代大小即可。

3)栈溢出:java.lang.StackOverflowError : Thread Stack space

栈溢出了,JVM 依然是采用栈式的虚拟机,这个和 C 和 Pascal 都是一样的。函数的调用过程都体现在堆栈和退栈上了。调用构造函数的“层”太多了,以致于把栈区溢出了。通常来讲,一般栈区远远小于堆区的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K 的空间(这个大约相当于在一个 C 函数内声明了 256 个 int 类型的变量),那么栈区也不过是需要 1MB 的空间。通常栈的大小是 1 -2MB 的。通俗一点讲就是单线程的程序需要的内存太大了。通常递归也不要递归的层次过多,很容易溢出。

解决方法 :1:修改程序。2:通过 -Xss: 来设置每个线程的 Stack 大小即可。

4)OutOfMemoryError:unable to create native thread

OutOfMemoryError:unable to create native thread:字面意思是内存溢出:无法创建新的线程。字面意思已经很明显了,出现这种情况的原因基本下面 2 点:

  • 程序创建的线程数超过操作系统的限制。
  • JVM 占用的内存太多,导致创建线程的内存空间太小。

我们都知道操作系统对每个进程的内存是有限制的,我们启动 Jvm, 相当于启动了一个进程,假如我们一个进程占用了 4G 的内存,那么通过下面的公式计算出来的剩余内存就是建立线程栈的时候可以用的内存。线程栈总可用内存 =4G-(-Xmx 的值)-(-XX:MaxPermSize 的值)- 程序计数器占用的内存 通过上面的公式我们可以看出,-Xmx 和 MaxPermSize 的值越大,那么留给线程栈可用的空间就越小,在 -Xss 参数配置的栈容量不变的情况下,可以创建的线程数也就越小。因此如果是因为这种情况导致的 unable to create native thread,

解决方法 :1:增大进程所占用的总内存。2:减少 -Xmx 或者 -Xss 来达到创建更多线程的目的。

5)小结

栈内存溢出:程序所要求的栈深度过大导致。
堆内存溢出:分清 内存泄露还是 内存容量不足。泄露则看对象如何被 GC Root 引用。不足则通过 调大 -Xms,-Xmx 参数。
持久带内存溢出:Class 对象未被释放,Class 对象占用信息过多,有过多的 Class 对象。
无法创建本地线程:总容量不变,堆内存,非堆内存设置过大,会导致能给线程的内存不足。

补充:阿里巴巴内存溢出面试题

下面哪种情况会导致持久区 jvm 堆内存溢出():

A. 循环上万次的字符串处理

B. 在一段代码内申请上百 M 甚至上 G 的内存

C. 使用 CGLib 技术直接操作字节码运行,生成大量的动态类

D. 不断创建对象

解答:AC

解析:AC 是持久带,B 直接内存也就是堆外内存,D 堆内存。

参考书籍:

《深入理解 Java 虚拟机》(第二版) 周志明 著;

温馨提示:如果你喜欢本文,并且想要学习更多干货内容,可以关注一下我的公众号《Java 技术 zhai》;
不定期的技术干货内容分享,带你重新理解架构的魅力!

退出移动版