内存泄露与内存溢出的区别及解决方法

内存溢出与泄露的区别

内存溢出 out of memory,指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存泄露 memory leak,指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光

memory leak会最终会导致out of memory!

    内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。 
    内存泄漏是指本应该被GC回收的无用对象没有被回收,导致的内存空间的浪费,当内存泄露严重时会导致OOMJava内存泄露根本原因是:长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被GC回收。

内存泄漏

内存泄漏的分类

以发生的方式来分类,内存泄漏可以分为4类: 

1. 常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。 
2. 偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。 
3. 一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。 
4. 隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

怎么避免内存泄露

  1. 尽量少使用枚举, 因为枚举是常量的一个集合, 你只是使用其中一个, 内部的所有枚举都会加载出来。
  2. 尽量使用静态内部类而不是内部类,因为如果内部类中做耗时操作,因为它会持有外部类的实例对象,导致外部类的实例在生命周期结束的时候没有办法及时释放,这就造成了内存泄漏。
  3. 尽量使用轻量级的数据结构, 在不使用的时候及记得即使使用clear()方法。
  4. 养成关闭连接和注销监听器的习惯, 在开启任何东西前把关闭都放在finally代码块中。                                                                                          在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。                                                     数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
  5. 谨慎使用static关键字, 使用static表示这是一个静态量, JVM就会立即加载它, 如果不使用的话有一定的内存浪费。
  6. 谨慎使用单例模式, 单例模式好是好, 但是还是要确保这个单例一定是常使用到的, 而且最好是使用双重检验的懒汉模式下的单例模式。

典型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虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发 OutOfMemoryError(下文称OOM )异常的可能。

常见的几种内存溢出的异常情况

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

1、Java堆溢出

        Java堆用于储存对象实例,我们只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
        限制Java 堆的大小为 20MB ,不可扩展(将 堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展 ),通过参数-XX +HeapDumpOnOutOf-MemoryError 可以让虚拟机在出现内存溢出异常的时候Dump 出当前的内存堆转储快照以便进行事后分析
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 space
Dumping heap to java_pid3404.hprof ...
Heap dump file created [22045981 bytes in 0.663 secs]
        Java堆内存的 OutOfMemoryError 异常是实际应用中最常见的内存溢出异常情况。出现 Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError” 会跟随进一步提示 “Java heap space”
        要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对 Dump 出来的堆转储快照进行分析。第一步首先应确认内存中导致 OOM 的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak )还是内存溢出( Memory Overflow)。下图 显示了使用 Eclipse Memory Analyzer 打开的堆转储快照文件。

在这里插入图片描述

  • 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
  • 如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗

2、虚拟机栈和本地方法栈溢出

        由于 HotSpot虚拟机中并不区分虚拟机栈和本地方法栈 ,因此对于 HotSpot 来说, -Xoss 参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的, 栈容量只能由-Xss参数来设定 。关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:
  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  2. 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
        《Java虚拟机规范》明确允许Java 虚拟机实现自行选择是否支持栈的动态扩展,而 HotSpot虚拟机的选择是不支持扩展所以除非在创建线程申请内存时就因无法获得足够内存而出现 OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的 只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
划重点:HotSpot虚拟机只会发生栈深度溢出的异常!
        为了验证这点,我们可以做两个实验,先将实验范围限制在单线程中操作,尝试下面两种行为是否能让HotSpot 虚拟机产生 OutOfMemoryError 异常。
第一种:使用-Xss 参数减少栈内存容量。
/**
* 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)
……后续异常堆栈信息省略
        对于不同版本的Java 虚拟机和不同的操作系统,栈容量最小值可能会有所限制,这主要取决于操作系统内存分页大小。譬如上述方法中的参数-Xss128k 可以正常用于 32 Windows 系统下的 JDK 6 ,但是如果用于64 Windows 系统下的 JDK 11 ,则会提示栈容量最小不能低于 180K ,而在 Linux 下这个值则可能是228K ,如果低于这个最小限制, HotSpot 虚拟器启动时会给出如下提示:
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;
		}
	}
}
结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。
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)
……后续异常堆栈信息省略
         实验结果表明: 无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是 StackOverflowError 异常。可是如果在允许动态扩展栈容量大小的虚拟机上,相同代码则会导致不一样的情况。譬如远古时代的Classic 虚拟机,这款虚拟机可以支持动态扩展栈内存的容量,在Windows 上的 JDK 1.0.2 运行代码清单 2-5 的话(如果这时候要调整栈容量就应该改用-oss 参数了),得到的结果是:
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) 
……后续异常堆栈信息省略
        可见相同的代码在Classic 虚拟机中成功产生了 OutOfMemoryError 而不是 StackOver-flowError 异常。如果测试时不限于单线程,通过不断建立线程的方式,在HotSpot 上也是可以产生内存溢出异常的,具体如下代码示。但是这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关
系,主要取决于操作系统本身的内存使用状态。甚至可以说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
/**
* 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的单个进程 最大内存限制为2GBHotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值,剩余的内存即为2GB(操作系统限制)减去最大堆容量,再减去最大方法区容量,由于程序计数器 消耗内存很小,可以忽略掉,如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存 就由虚拟机栈和本地方法栈来分配了因此为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽,上述代码就演示了这种情况。

        重点提示一下,如果要尝试运行上面这段代码,记得要先保存当前的工作,由于在Windows平台的虚拟机中, Java 的线程是映射到操作系统的内核线程上 ,无限制地创建线程会对操作系统带来很大压力,上述代码执行时有很高的风险,可能会由于创建线程数量过多而导致操作系统假死。在32 位操作系统下的运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
        出现StackOverflowError 异常时,会有明确错误堆栈可供分析,相对而言比较容易定位到问题所在。如果使用HotSpot 虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说大多数情况下)到达1000~2000 是完全没有问题,对于正常的方法调用(包括不能做尾递归优化的递归调用),这个深度应该完全够用了。但是, 如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程 。这种通过“ 减少内存 的手段来解决内存溢出的方式,如果没有这方面处理经验,一般比较难以想到,这一点读者需要在开发32 位系统的多线程应用时注意。也是由于这种问题较为隐蔽,从 JDK 7起,以上提示信息中 “unable to create native thread” 后面,虚拟机会特别注明原因可能是 “possibly out of memory or process/resource limits reached”

3、方法区和运行时常量池溢出

        由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。前面曾经提到HotSpot JDK 7 开始逐步 去永久代 的计划,并在 JDK 8 中完全使用元空间来代替永久代。
元空间与永久代之间最大的区别在于

永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
        在此我们就以测试代码来观察一下,使用“永久代 还是 元空间 ”来实现方法区,对程序有什么实际的影响。        
       
          String::intern()是一个本地方法,它的作用 是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用
        在JDK 6 或更早之前的 HotSpot 虚拟机中,常量池都是分配在永久代中,我们可以通过-XX PermSize -XX MaxPermSize 限制永久代的大小,即可间接限制其中常量池的容量,具体实现如下方代码 所示:
//运行时常量池导致的内存溢出异常   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 space
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)
        从运行结果中可以看到,运行时常量池溢出时,在OutOfMemoryError 异常后面跟随的提示信息是“PermGen space”,说明运行时常量池的确是属于方法区(即JDK 6的HotSpot虚拟机中的永久代)的一部分
        而使用JDK 7 或更高版本的 JDK 来运行这段程序并不会得到相同的结果,无论是在 JDK 7 中继续使用-XX MaxPermSize 参数或者在 JDK 8 及以上版本使用 -XX MaxMeta-spaceSize 参数把方法区容量同样限制在6MB ,也都不会重现 JDK 6 中的溢出异常,循环将一直进行下去,永不停歇 。出现这种变化,是因为自JDK 7 起,原本存放在永久代的 字符串常量池被移至Java堆之中,所以在JDK 7 及以上版本,限制方法区的容量对该测试用例来说是毫无意义的。这时候 使用-Xmx参数 限制最大堆到 6MB 就能够看到以下两种运行结果之一,具体取决于哪里的对象分配时产生了溢出:
// OOM 异常一:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at 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 space
at 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 中运行,会得到两个 false ,而在 JDK 7 中运行,会得到一个 true 和一个 false
产生差异的原因是:
        在JDK 6中, intern() 方法会把 首次遇到的字符串实例复制到永久代的字符串常量池中 存储, 返回的也是永久代里面这个字符串实例 的引用,而由StringBuilder 创建的字符串对象实例 在Java堆上 ,所以必然不可能是同一个引用,结果将返回 false
        而JDK 7 (以及部分其他虚拟机,例如 JRockit )的 intern() 方法实现就不需要再拷贝字符串的实例到永久代了既然字符串常量池已经移到Java 堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder 创建的那个字符串实例就是同一个。而对 str2 比较返回false ,这是因为 “java” 这个字符串在执行 String-Builder.toString() 之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern() 方法要求 首次遇到 的原则, 计算机软件 这个字符串则是首次出现的,因此结果返回true
        我们再来看看方法区的其他部分的内容,方法区的主要职责是用于 存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述 。对于这部分区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出为止。虽然直接使用Java SE API 也可以动态产生类(如反射时的GeneratedConstructorAccessor和动态代理等),但在本次实验中操作起来比较麻烦。在如下代码 里,借助了CGLib 直接操作字节码运行时生成了大量的动态类。        
//借助 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 space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
... 8 more
        值得特别注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,类似这样的代码确实可 能会出现在实际应用中:当前的很多主流框架,如Spring Hibernate 对类进行增强时,都会使用到 CGLib这类字节码技术,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内 存。另外,很多运行于 Java 虚拟机上的动态语言(例如 Groovy 等)通常都会持续创建新类型来支撑语言的动态性,随着这类动态语言的流行,与上述代码 相似的溢出场景也越来越容易遇到。
        方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,要达成的条件是比较苛刻的。在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。这类场景除了之前提到的程序使用了CGLib 字节码增强和动态语言外,常见的还有:大量 JSP 或动态产生 JSP文件的应用(JSP第一次运行时需要编译为 Java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。
        在JDK 8 以后,永久代便完全退出了历史舞台,元空间作为其替代者登场。在默认设置下,前面列举的那些正常的动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区的溢出异常了。不过为了让使用者有预防实际应用里出现类似于代码清单2-9 那样的破坏性的操作, HotSpot 还是提供了一些参数作为元空间的防御措施,主要包括:
  1. -XXMaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
  2. -XXMetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
  3. -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XXMax-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。

4、本机直接内存溢出

        直接内存(Direct Memory )的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx 指定)一致,下发代码 越过了 DirectByteBuffer 类直接通过反射获取Unsafe 实例进行内存分配( Unsafe 类的 getUnsafe() 方法指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe 的功能,在 JDK 10 时才将 Unsafe的部分功能通过VarHandle 开放给外部使用),因为虽然使用 DirectByteBuffer 分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe::allocateMemory()
//使用 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.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)

  • 8
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值