内存溢出与泄露的区别
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
内存泄漏是指本应该被GC回收的无用对象没有被回收,导致的内存空间的浪费,当内存泄露严重时会导致OOM。Java内存泄露根本原因是:长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被GC回收。
内存泄漏
内存泄漏的分类
以发生的方式来分类,内存泄漏可以分为4类:
1. 常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
2. 偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
3. 一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
4. 隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
怎么避免内存泄露
- 尽量少使用枚举, 因为枚举是常量的一个集合, 你只是使用其中一个, 内部的所有枚举都会加载出来。
- 尽量使用静态内部类而不是内部类,因为如果内部类中做耗时操作,因为它会持有外部类的实例对象,导致外部类的实例在生命周期结束的时候没有办法及时释放,这就造成了内存泄漏。
- 尽量使用轻量级的数据结构, 在不使用的时候及记得即使使用clear()方法。
- 养成关闭连接和注销监听器的习惯, 在开启任何东西前把关闭都放在finally代码块中。 在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。 数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
- 谨慎使用static关键字, 使用static表示这是一个静态量, JVM就会立即加载它, 如果不使用的话有一定的内存浪费。
- 谨慎使用单例模式, 单例模式好是好, 但是还是要确保这个单例一定是常使用到的, 而且最好是使用双重检验的懒汉模式下的单例模式。
典型DEMO:
1.静态集合类引起内存泄露:
像HashMap、Vector等集合的使用最容易出现内存泄露。因为这些集合属于静态集合,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object(); //每次创建新的对象
v.add(o);
o = null; //将对象添加到集合后将对象的引用置空
}
//因为对象的引用置空之后,JVM已经失去的使用该对象的价值,本应该被GC清除,但是在vector集合中还存在着此对象的引用,
//导致没能顺利清除
循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将v = null。这样就可以将Vector执行那个的对象也释放。
2、当集合(Hash算法的集合)里面的对象属性被修改后,再调用remove()方法时不起作用
public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孙悟空","pwd2",26);
Person p3 = new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
for (Person person : set)
{
System.out.println(person);
}
}
内存溢出
常见的几种内存溢出的异常情况
- 堆溢出(java.lang.OutOfMemoryError:java heap space)
- 持久代溢出(java.lang.OutOfMemoryError: PermGen space)
- 栈溢出(java.lang.StackOverflowError)
- OutOfMemoryError:unable to create native thread
1、Java堆溢出
Java堆内存溢出异常测试/*** VM Args :-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError* @author zzm*/public class HeapOOM {static class OOMObject {}public static void main(String[] args) {List<OOMObject> list = new ArrayList<OOMObject>();while (true) {list.add(new OOMObject());}}}运行结果:java.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid3404.hprof ...Heap dump file created [22045981 bytes in 0.663 secs]
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
- 如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
2、虚拟机栈和本地方法栈溢出
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
/**
* VM Args:-Xss128k
* @author zzm
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。
stack length:2402
Exception in thread “main” java.lang.StackOverflowError
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:20)
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:21)
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:21)
……后续异常堆栈信息省略
The Java thread stack size specified is too small. Specify at least 228k
/**
* @author zzm
*/
public class JavaVMStackSOF {
private static int stackLength = 0;
public static void test() {
long unused1, unused2, unused3, unused4, unused5,unused6, unused7, unused8, unused9, unused10,
unused11, unused12, unused13, unused14, unused15,unused16, unused17, unused18, unused19, unused20,
unused21, unused22, unused23, unused24, unused25,unused26, unused27, unused28, unused29, unused30,
unused31, unused32, unused33, unused34, unused35,unused36, unused37, unused38, unused39, unused40,
unused41, unused42, unused43, unused44, unused45,unused46, unused47, unused48, unused49, unused50,
unused51, unused52, unused53, unused54, unused55,unused56, unused57, unused58, unused59, unused60,
unused61, unused62, unused63, unused64, unused65,unused66, unused67, unused68, unused69, unused70,
unused71, unused72, unused73, unused74, unused75,unused76, unused77, unused78, unused79, unused80,
unused81, unused82, unused83, unused84, unused85,unused86, unused87, unused88, unused89, unused90,
unused91, unused92, unused93, unused94, unused95,unused96, unused97, unused98, unused99, unused100;
stackLength ++;
test();
unused1 = unused2 = unused3 = unused4 = unused5 =unused6 = unused7 = unused8 = unused9 = unused10 =
unused11 = unused12 = unused13 = unused14 = unused15 =unused16 = unused17 = unused18 = unused19 = unused20 =
unused21 = unused22 = unused23 = unused24 = unused25 =unused26 = unused27 = unused28 = unused29 = unused30 =
unused31 = unused32 = unused33 = unused34 = unused35 =unused36 = unused37 = unused38 = unused39 = unused40 =
unused41 = unused42 = unused43 = unused44 = unused45 =unused46 = unused47 = unused48 = unused49 = unused50 =
unused51 = unused52 = unused53 = unused54 = unused55 =unused56 = unused57 = unused58 = unused59 = unused60 =
unused61 = unused62 = unused63 = unused64 = unused65 =unused66 = unused67 = unused68 = unused69 = unused70 =
unused71 = unused72 = unused73 = unused74 = unused75 =unused76 = unused77 = unused78 = unused79 = unused80 =
unused81 = unused82 = unused83 = unused84 = unused85 =unused86 = unused87 = unused88 = unused89 = unused90 =
unused91 = unused92 = unused93 = unused94 = unused95 =unused96 = unused97 = unused98 = unused99 = unused100 = 0;
}
public static void main(String[] args) {
try {
test();
}catch (Error e){
System.out.println("stack length:" + stackLength);
throw e;
}
}
}
stack length:5675
Exception in thread “main” java.lang.StackOverflowError
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:27)
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:28)
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:28)
……后续异常堆栈信息省略
stack length:3716 java.lang.OutOfMemoryError at org.fenixsoft.oom.
JavaVMStackSOF.leak(JavaVMStackSOF.java:27) at org.fenixsoft.oom.
JavaVMStackSOF.leak(JavaVMStackSOF.java:28) at org.fenixsoft.oom.
JavaVMStackSOF.leak(JavaVMStackSOF.java:28)
……后续异常堆栈信息省略
/**
* VM Args:-Xss2M (这时候不妨设大些,请在32位系统下运行)
* @author zzm
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
原因其实不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程 最大内存限制为2GB。HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值,那剩余的内存即为2GB(操作系统限制)减去最大堆容量,再减去最大方法区容量,由于程序计数器 消耗内存很小,可以忽略掉,如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存 就由虚拟机栈和本地方法栈来分配了。因此为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽,上述代码就演示了这种情况。
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
3、方法区和运行时常量池溢出
元空间与永久代之间最大的区别在于:
永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
//运行时常量池导致的内存溢出异常 jdk6 运行/*** VM Args : -XX:PermSize=6M -XX:MaxPermSize=6M* @author zzm*/public class RuntimeConstantPoolOOM {public static void main(String[] args) {// 使用Set 保持着常量池引用,避免 Full GC 回收常量池行为Set<String> set = new HashSet<String>();// 在short 范围内足以让 6MB 的 PermSize 产生 OOM 了short i = 0;while (true) {set.add(String.valueOf(i++).intern());}}}运行结果:Exception in thread "main" java.lang.OutOfMemoryError: PermGen spaceat java.lang.String.intern(Native Method)at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)
// OOM 异常一:Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.base/java.lang.Integer.toString(Integer.java:440)at java.base/java.lang.String.valueOf(String.java:3058)at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)// OOM 异常二:Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.base/java.util.HashMap.resize(HashMap.java:699)at java.base/java.util.HashMap.putVal(HashMap.java:658)at java.base/java.util.HashMap.put(HashMap.java:607)at java.base/java.util.HashSet.add(HashSet.java:220)at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)
// String.intern() 返回引用的测试public class RuntimeConstantPoolOOM {public static void main(String[] args) {String str1 = new StringBuilder("计算机").append(" 软件 ").toString();System.out.println(str1.intern() == str1);String str2 = new StringBuilder("ja").append("va").toString();System.out.println(str2.intern() == str2);}}
在JDK 6中, intern() 方法会把 首次遇到的字符串实例复制到永久代的字符串常量池中 存储, 返回的也是永久代里面这个字符串实例 的引用,而由StringBuilder 创建的字符串对象实例 在Java堆上 ,所以必然不可能是同一个引用,结果将返回 false 。而JDK 7 (以及部分其他虚拟机,例如 JRockit )的 intern() 方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java 堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder 创建的那个字符串实例就是同一个。而对 str2 比较返回false ,这是因为 “java” 这个字符串在执行 String-Builder.toString() 之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern() 方法要求 “ 首次遇到 ” 的原则, “ 计算机软件 ” 这个字符串则是首次出现的,因此结果返回true 。
//借助 CGLib 使得方法区出现内存溢出异常/*** VM Args : -XX:PermSize=10M -XX:MaxPermSize=10M* @author zzm*/public class JavaMethodAreaOOM {public static void main(String[] args) {while (true) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor() {public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {return proxy.invokeSuper(obj, args);}});enhancer.create();}}static class OOMObject {}}在JDK 7中的运行结果:Caused by: java.lang.OutOfMemoryError: PermGen spaceat java.lang.ClassLoader.defineClass1(Native Method)at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)at java.lang.ClassLoader.defineClass(ClassLoader.java:616)... 8 more
- -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
- -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
- -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。
4、本机直接内存溢出
//使用 unsafe 分配本机内存/*** VM Args : -Xmx20M -XX:MaxDirectMemorySize=10M* @author zzm*/public class DirectMemoryOOM {private static final int _1MB = 1024 * 1024;public static void main(String[] args) throws Exception {Field unsafeField = Unsafe.class.getDeclaredFields()[0];unsafeField.setAccessible(true);Unsafe unsafe = (Unsafe) unsafeField.get(null);while (true) {unsafe.allocateMemory(_1MB);}}}运行结果:Exception in thread "main" java.lang.OutOfMemoryErrorat sun.misc.Unsafe.allocateMemory(Native Method)at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)