当谈到 JVM 内存问题时,经常听到 "内存泄漏" 和 "内存溢出" 这两个术语。以下是它们的详细解释:
内存泄漏指的是程序中已不再需要的内存却未被释放的情况。这些未释放的内存会逐渐积累,最终导致系统内存耗尽。在 Java 中,内存泄漏通常是由于对对象的引用没有被释放或清理所导致的。
内存溢出指的是应用程序申请的内存超过了虚拟机所能提供的内存大小。在 Java 中,最常见的内存溢出异常是 java.lang.OutOfMemoryError
。
程序所要求的栈深度过大导致,可以写一个死递归程序触发。
堆内存溢出(OutOfMemoryError:java heap space)
分清内存溢出还是内存泄漏
泄露则看对象如何被 GC Root 引用。
溢出则通过 调大 -Xms,-Xmx参数。
持久带内存溢出(OutOfMemoryError: PermGen space)
持久带中包含方法区,方法区包含常量池
因此持久带溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置
用String.intern()触发常量池溢出
Class对象未被释放,Class对象占用信息过多,有过多的Class对象。可以导致持久带内存溢出
无法创建本地线程
总容量不变,堆内存,非堆内存设置过大,会导致能给线程的内存不足。
栈溢出抛出StackOverflowError错误,出现此种情况是因为方法运行的时候栈的深度超过了虚拟机容许的最大深度所致。出现这种情况,一般情况下是程序错误所致的,比如写了一个死递归,就有可能造成此种情况。下面我们通过一段代码来模拟一下此种情况的内存溢出。
import java.util.*;
import java.lang.*;
public class OOMTest{
public void stackOverFlowMethod(){
stackOverFlowMethod();
}
public static void main(String... args){
OOMTest oom = new OOMTest();
oom.stackOverFlowMethod();
}
}
运行上面的代码,会抛出如下的异常:
Exception in thread "main" java.lang.StackOverflowError
at OOMTest.stackOverFlowMethod(OOMTest.java:6)
对于栈内存溢出,根据《Java 虚拟机规范》中文版:
如果线程请求的栈容量超过栈允许的最大容量的话,Java 虚拟机将抛出一个StackOverflow异常;如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。
堆内存溢出的时候,虚拟机会抛出java.lang.OutOfMemoryError:java heap space,出现此种情况的时候,我们需要根据内存溢出的时候产生的dump文件来具体分析(需要增加-XX:+HeapDumpOnOutOfMemoryErrorjvm启动参数)。出现此种问题的时候有可能是内存泄露,也有可能是内存溢出了。
如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。
如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题。下面我们通过如下的代码来演示一下此种情况的溢出:
import java.util.*;
import java.lang.*;
public class OOMTest{
public static void main(String... args){
List buffer = new ArrayList();
buffer.add(new byte[10*1024*1024]);
}
}
我们通过如下的命令运行上面的代码:
java -verbose:gc -Xmn10M -Xms20M -Xmx20M -XX:+PrintGC OOMTest
程序输出如下的信息:
[GC 1180K->366K(19456K), 0.0037311 secs]
[Full GC 366K->330K(19456K), 0.0098740 secs]
[Full GC 330K->292K(19456K), 0.0090244 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at OOMTest.main(OOMTest.java:7)
从运行结果可以看出,JVM进行了一次Minor gc和两次的Major gc,从Major gc的输出可以看出,gc以后old区使用率为134K,而字节数组为10M,加起来大于了old generation的空间,所以抛出了异常,如果调整-Xms21M,-Xmx21M,那么就不会触发gc操作也不会出现异常了。
通过上面的实验其实也从侧面验证了一个结论:对象大于新生代剩余内存的时候,将直接放入老年代,当老年代剩余内存还是无法放下的时候,触发垃圾收集,收集后还是不能放下就会抛出内存溢出异常了。
最后我们在来看看java.lang.OutOfMemoryError:unable to create natvie thread这种错误。出现这种
情况的时候,一般是下面两种情况导致的:
1. 程序创建的线程数超过了操作系统的限制。对于Linux系统,我们可以通过ulimit -u来查看此限制。
2. 给虚拟机分配的内存过大,导致创建线程的时候需要的native内存太少。
我们都知道操作系统对每个进程的内存是有限制的,我们启动Jvm,相当于启动了一个进程,假如我们一个进程占用了4G的内存,那么通过下面的公式计算出来的剩余内存就是建立线程栈的时候可以用的内存。线程栈总可用内存=4G-(-Xmx的值)- (-XX:MaxPermSize的值)- 程序计数器占用的内存,通过上面的公式我们可以看出,-Xmx 和 MaxPermSize的值越大,那么留给线程栈可用的空间就越小,在-Xss参数配置的栈容量不变的情况下,可以创建的线程数也就越小。因此如果是因为这种情况导致的unable to create native thread,那么要么我们增大进程所占用的总内存,或者减少-Xmx或者-Xss来达到创建更多线程的目的。
针对内存泄漏,开发人员可以通过注意对象的生命周期、避免不必要的引用持有以及确保及时释放资源等方式来预防和解决内存泄漏问题。
而对于内存溢出,通常需要优化代码逻辑,减少内存消耗,或者增加 JVM 的内存限制,或者使用一些性能分析工具来识别内存瓶颈并进行优化。