【深入理解JAVA虚拟机学习笔记三】栈溢出,堆溢出,直接内存溢出,内存泄漏,以及异常信息

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

由于在Hotspot虚拟机中中不区分虚拟机栈和本地方法栈,因此-Xoss(本地方法栈大小)修改参数是无效的,栈容量只由-Xss参数设定。

  • 如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。

这两种异常有一些重叠的部分:
当栈空间无法继续分配时,到底是内存太小,还是已经使用的栈空间过大,其本质只是对同一件事情的两种不同描述。
可以通过一下方法验证:

  • 在使用-Xss参数减少栈内存容量,结果抛出Stack OverflowError异常,异常出现时输出的堆栈深度相应缩小。
  • 定义了大量的本地变量,增大此方法栈中本地变量表的长度,结果抛出Stack OverflowError异常时输出的堆栈深度相应缩小。

可以通过递归调用的方式进行测试:

public void stackLeak() {
	stackLeak();
}
//运行结果
Exception in thread "main" java.lang.StackOverflowError

通过不断建立线程的方式可以生产内存异常异常,但是产生的内存异常异常和栈空间是否足够大并不存在任何关联,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出。
操作系统为虚拟机分配的内存时有限制的,如果虚拟机进程本身消耗的内存计算在内,剩余的内存就由虚拟机栈和本地方法栈瓜分了,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
如果是建立线程过多导致内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
可以通过死循环创建线程的方式模拟“由于线程过多导致的内存溢出”:

    while(true){
    	Thread t = new Thread....
    }

在这里插入图片描述

Java堆溢出

Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆得容量限制后就会产生内存溢出异常。堆大小由-Xmx和-Xms来调节,可以自己设置。
java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“java heap space”.

 static class OOMOBject {
        int a = 4;
        String s = "4564654654";
    }
    public static void main(String[] args) {
        List<OOMOBject> list = new ArrayList<>();
        while(true) {
            list.add(new OOMOBject());
        }
    }

要解决这个区域的异常,一般的手段是首先通过内存映像分析工具对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(memory leak)还是内存溢出(Menory OverFlow)

  • 如果是内存泄漏(Memory Leak),可进一步通过工具查看泄漏对象到GC Roots的引用链,于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收他们的,掌握了泄露对象的类型信息以及GC Roots引用链的信息,就可以比较精准地定位出泄漏代码的位置。
  • 如果不存在泄漏,换句话说就是内存中的对象确实必须存活着不能被GC掉,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期间的内存消耗
    每new一次就会在堆里面占用一次内存,list.add保证了new出来的对象不会被GC,最后java堆放不下,就炸了

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

String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。在JDK1.6及之前版本中,由于常量池分配在永久代中(即方法区),我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量,注意,JDK1.7开始逐步开始“去永久代”。代码如下所示:

	public static void main(String[] args) {
        // 使用List保持着常量池引用,避免Full GC回收常量池行为
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
    //运行结果
    Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    	at java.lang.String.intern(Native Method)
    	at jvm.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:16)

从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。但是使用JDK1.7运行这段程序不会得到相同的结果,而是出现以下的提示信息,这是因为这两个参数已经不在JDK1.7中使用了。
如果在JDK1.7中运行RuntimeConstantPoolOOM.java程序,while循环将一直运行下去,但是,while循环并不是始终运行下去,直到系统中堆内存用完为止,一般需要过好长时间才会出现,不过笔者并没有在本地测试。因为在JDK1.7中常量池存储的不再是对象,而是对象引用,真正的对象是存储在堆中的。

本机直接内存溢出

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认和Java堆最大值(-Xmx)一样

	// 设置降低直接内存的空间来加快异常的抛出
	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 test_DirectMemoryOOM.DirectrMemoryOOM.main(DirectrMemoryOOM.java:15)

由DirectrMemory导致的内存溢出,一个明显的特征是Heap Dump中文件不会看见明显的异常。

内存溢出的多种情况总结

第一类内存溢出,也是大家认为最多,第一反应认为是的内存溢出,就是堆栈溢出

那什么样的情况就是堆栈溢出呢?当你看到下面的关键字的时候它就是堆栈溢出了:

java.lang.OutOfMemoryError: ......java heap space.....

也就是当你看到heap相关的时候就肯定是堆栈溢出了,此时如果代码没有问题的情况下,适当调整-Xmx和-Xms是可以避免的,不过一定是代码没有问题的前提,为什么会溢出呢,要么代码有问题,要么访问量太多并且每个访问的时间太长或者数据太多,导致数据释放不掉,因为垃圾回收器是要找到那些是垃圾才能回收,这里它不会认为这些东西是垃圾,自然不会去回收了;主意这个溢出之前,可能系统会提前先报错关键字为:

java.lang.OutOfMemoryError:GC over head limit exceeded

这种情况是当系统处于高频的GC状态,而且回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放的对象,有可能是引用使用不当导致,或申请大对象导致,但是java heap space的内存溢出有可能提前不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC.

第二类内存溢出,PermGen的溢出,或者PermGen 满了的提示,你会看到这样的关键字:

关键信息为:

java.lang.OutOfMemoryError: PermGen space

原因:系统的代码非常多或引用的第三方包非常多、或代码中使用了大量的常量、或通过intern注入常量、或者通过动态代码加载等方法,导致常量池的膨胀,虽然JDK 1.5以后可以通过设置对永久带进行回收,但是我们希望的是这个地方是不做GC的,它够用就行,所以一般情况下今年少做类似的操作,所以在面对这种情况常用的手段是:增加-XX:PermSize和-XX:MaxPermSize的大小。

第三类内存溢出:在使用ByteBuffer中的allocateDirect()的时候会用到,很多javaNIO的框架中被封装为其他的方法

溢出关键字:

java.lang.OutOfMemoryError: Direct buffer memory

如果你在直接或间接使用了ByteBuffer中的allocateDirect方法的时候,而不做clear的时候就会出现类似的问题,常规的引用程序IO输出存在一个内核态与用户态的转换过程,也就是对应直接内存与非直接内存,如果常规的应用程序你要将一个文件的内容输出到客户端需要通过OS的直接内存转换拷贝到程序的非直接内存(也就是heap中),然后再输出到直接内存由操作系统发送出去,而直接内存就是由OS和应用程序共同管理的,而非直接内存可以直接由应用程序自己控制的内存,jvm垃圾回收不会回收掉直接内存这部分的内存,所以要注意了哦。

如果经常有类似的操作,可以考虑设置参数:-XX:MaxDirectMemorySize

第四类内存溢出错误:

溢出关键字:

java.lang.StackOverflowError

这个参数直接说明一个内容,就是-Xss太小了,我们申请很多局部调用的栈针等内容是存放在用户当前所持有的线程中的,线程在jdk 1.4以前默认是256K,1.5以后是1M,如果报这个错,只能说明-Xss设置得太小,当然有些厂商的JVM不是这个参数,本文仅仅针对Hotspot VM而已;不过在有必要的情况下可以对系统做一些优化,使得-Xss的值是可用的。

第五类内存溢出错误:

溢出关键字:

java.lang.OutOfMemoryError: unable to create new native thread

上面第四种溢出错误,已经说明了线程的内存空间,其实线程基本只占用heap以外的内存区域,也就是这个错误说明除了heap以外的区域,无法为线程分配一块内存区域了,这个要么是内存本身就不够,要么heap的空间设置得太大了,导致了剩余的内存已经不多了,而由于线程本身要占用内存,所以就不够用了,说明了原因,如何去修改,不用我多说,你懂的。

第六类内存溢出:

溢出关键字

java.lang.OutOfMemoryError: request {} byte for {}out of swap

这类错误一般是由于地址空间不够而导致。

参考:https://www.cnblogs.com/chbin/p/10656566.html

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值