关于JVM内存溢出的原因分析及解决方案探讨
前言:jvm中除了程序计数器,其他的区域都有可能会发生内存溢出。
0.什么是内存溢出
当程序需要申请内存的时候,由于没有足够的内存,此时就会抛出outofmemoryerror,这就是内存溢出。
1.内存泄漏和内存溢出区别与联系
- 内存泄漏:系统分配的内存没有被回收。
- 内存溢出:分配的内存空间超过系统内存。
2.内存泄漏的原因分析
jvm由5大块组成:堆,栈,本地方法栈,程序计数器,方法区。栈它的主要记录方法的执行和对象的引用。堆则存在真正的引用的对象。
内存泄漏是由于使用不当,把一部分内存“丢掉了”,导致这部分内存不可用。
当在堆中创建了对象,后来没有使用这个对象了,又没有把整个对象的相关引用设为null。此时垃圾收集器会认为这个对象是需要的,就不会清理这部分内存。这就会导致这部分内存不可用。
所以内存泄漏会导致可用的内存减少,进而会导致内存溢出。
3. jvm垃圾回收机制思想
就是从栈出发(root),遍历对象的引用,在遍历堆里面的引用对象,因为栈中的对象的引用执行完毕就删除,所以我们就可以通过栈中的对象的引用,查找到堆中没有被指向的对象,这些对象即为不可到达对象,对其进行垃圾回收。
4.内存溢出的原因分析
内存溢出是由于没被引用的对象(垃圾)过多造成jvm没有及时回收,造成的内存溢出。如果出现这种现象可行代码排查:
- 是否app中的类中和引用变量过多使用了static修饰 如public staitc student s;在类中的属性中使用 static修饰的最好只用基本类型或字符串。如public static int i = 0; //public static string str;
- 是否app中使用了大量的递归或无限递归(递归中用到了大量的建新的对象)
- 是否app中使用了大量循环或死循环(循环中用到了大量的新建的对象)
- 检查app中是否使用了向数据库查询所有记录的方法。即一次性全部查询的方法,如果数据量超过10万多条了,就可能会造成内存溢出。所以在查询时应采用“分页查询”。
- 检查是否有数组,list,map中存放的是对象的引用而不是对象,因为这些引用会让对应的对象不能被释放。会大量存储在内存中。
- 检查是否使用了“非字面量字符串进行+”的操作。因为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.常见的四种内存溢出情况
- 堆溢出(outofmemoryerror:java heap space)
- 持久代溢出(outofmemoryerror: permgen space)
- 栈溢出(*error)
- 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.*error : 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虚拟机》 (第二版) 周志明 著;