二、JVM 各区域内存溢出汇总


前言

      在java虚拟机规范的描述中,处程序计数器外,虚拟机内存的其他几个运行区域(不清楚JVM各个区域划分请阅读上一篇文章)都有发生OutOfMemoryError异常的可能,本篇文章将通过若干个实例来验证异常发生的场景,并且会初步介绍几个与内存相关的最基本的虚拟机参数


一、准备

       本篇文章涉及到需要修改虚拟机参数的地方。idea修改jvm虚拟机参数的方法如下。快捷键 ctrl+alt+shift+s
在这里插入图片描述
在这里插入图片描述

二、实战:OutOfMemoryError

1.java 堆内存异常

      Java 堆用于存储对象实例,只要不断创建对象,并且保证GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么对象数量到达最大堆的容量限制后就会产生内存溢出异常。

堆相关虚拟机参数:
      -XX:+HeapDumpOnOutOfMemoryError → 虚拟机在出现内存OOM时Dump 出当前内存堆转储快照以便事后进行分析。
      -Xms → 设置堆的最小值
      -Xmx → 设置堆的最大值

实验:       

/**
 * Java 堆内存溢出案例
 * JDK1.8
 * vmoption -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
 */
public class HeapOOM {
    static class OOMObject{

    }
    public static void main(String[] args) {
        ArrayList<OOMObject> list = new ArrayList();
        while (true){
            list.add(new OOMObject());
        }
    }
}

运行结果:       
在这里插入图片描述
堆内存溢出异常标志: Java heap space

解决方法:
      先通过内存映像分析工具 eclipse-> (Eclipse Memory Analyzer) idea -> JProfilerl 对Dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的。也就是要先分清楚到底是出现了内存泄露(Memory leak)还是内存溢出(Memory Overflow)

内存泄露:进一步通过工具查看泄露对象到Gc Roots 的引用链 于是就能找到泄露对象是通过咋样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收他们 掌握了泄露对象的类型信息及GC Roots 引用链 就可以比较准确的定位出泄露代码的位置。

没有内存泄露:换句话说就是对象必须活着,那就应当检查虚拟机的堆参数 (-Xmx 与 -Xms) 与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况。尝试减少程序运行期间的内存消耗。具体详见下一篇文章。

2.虚拟机栈和本地方法栈内存异常

      由于HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,因此对于Hotspot来说虽然-Xoss 参数(设置本地方法栈大小)存在,但实际上是无效的。栈容量只有 -Xss 参数设定,java 虚拟机规范中描述了栈的两种异常:

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

栈相关虚拟机参数:
      -Xss → 栈容量

实验:       

/**
单线程实验
*/
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) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}
/**
多线程实验 提示:执行该案例可能会导致windows假死,实验之前请注意保存重要数据
*/
public class JavaVmStackSOFD {
    private int stackLength = 1;
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable{
        JavaVmStackSOFD oom = new JavaVmStackSOFD();
        try {
            oom.stackLeak();
        }catch (Throwable e){
            System.out.println("栈长度:"+oom.stackLength);
            throw e;
        }
    }
}

运行结果: 经过多次实验,发现单线程是不会出现内存溢出的都是报错StackOverflowError,但是多线程会内存溢出

栈内存溢出异常标志: StackOverflowError

分析:
      经过多次实验,发现单线程是不会出现内存溢出的都是报错StackOverflowError,但是多线程会内存溢出。
这是因为:windows 给每个进程分配的内存大小是有限制的,如 32位的限制是2GB 64位会翻倍, java 内存结构组成为(对JVM内存结构不清晰的可以看我上一篇文章):|程序计数器 |虚拟机栈| 本地方法栈| 堆|方法区
所以java进程的内存 = 程序计数器+虚拟机栈+本地方法栈+堆+方法区 = 2GB(假设window 32位)
那么:
(虚拟机栈+本地方法栈)的内存 = 2GB -(堆(-xmx)+方法区(-MaxPermSize))
程序计数器占用内存几乎忽略不计
所以 线程创建的多了,堆和方法区占用的内存就大了,此时栈的内存就小了,就会发生内存溢出。使用虚拟机默认参数栈深度在大多数情况下达到1000-2000完全没有问题。对于正常的方法调用,包括递归,这个深度完全够用了。但是如果建立过多的线程导致内存溢出、在不能减少线程数的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

3.方法区和运行时常量池内存溢出异常

      由于运行时常量池是方法区的一部分,因此这两个区域的内存溢出我放一起做实验分析:

穿插一个实验用到的方法的知识点:String.intern() 是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中的这个字符串的String 对象,否则将此String对象包含的字符串添加到常量池中。并且返回此String对象的引用

方法区相关的虚拟机参数:
      -XX:PermSize → 方法区容量
      -XX:MaxPermSize → 方法区最大内存
      -XX:MetaspaceSize → 初始空间大小,达到该值就会触发垃圾收集进行类型卸载(jdk1.8)
      -XX:MaxMetaspaceSize → 最大空间,默认是没有限制的(jdk1.8)

方法区→运行时常量池内存实验:       

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
 * 运行时常量溢出实验
 * jdk1.6 运行结果
 * Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
 * 	at java.lang.String.intern(Native Method)
 * 	at com.test.ch2.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:16)
 *
 * jdk1.7 和jdk1.8 会一直运行下去。
 * 1.6 运行时常量池属于方法区(HotSpot 虚拟机中的永久代的一部分)
 * 而1.7 已经有区永久化思想所以不会内存溢出
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i=0;
        while(true){
            list.add(String.valueOf(i++).intern());
        }

    }
}


运行结果:

jdk1.6:Exception in thread “main” java.lang.OutOfMemoryError: PermGen space
jdk1.7 和jdk1.8:一直运行下去。

方法区运行时常量池内存溢出异常标志: java.lang.OutOfMemoryError: PermGen space(永久代)

方法区 内存溢出实验:       

package com.test.ch2;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * 方法区内存溢出demo
 * 方法区:用于存放Class 的相关信息,如:类名、访问修饰符、常量池、字段描述、方法描述等。所以这个实验的思路就是运行时产生大量的类区填满方法区。
 * 实验思路就是  借助CGLIB 产生大量类。
 * 
 * 方法去溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的,在经常动态生成大量Class的应用中需要特别注意。
 *
 */
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() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o,objects);
                }
            });
            enhancer.create();
        }
    }
    static class OOMObject{

    }
}

运行结果:

jdk版本实验结果
1.8一直运行下去
1.7Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread “main”
1.6Caused by: java.lang.OutOfMemoryError: PermGen space

分析:

  • 方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本思路都是运行时产生大量的类区填满方法区。
  • 由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出尤其是大量jsp或者动态反射产生大量类文件的应用,jdk1.6 HotSpot 方法区概念没有发生变化,所以这里就明确指出是永久区内存溢出。
  • JDK1.7开始出现去永久化的思想、存储在永久代的部分数据就已经转移到Java Heap或者Native Heap。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap.从实验现象笔者猜想出现的原因是由于这个时候去永久代思想过度期,方法区已经是一个概念上的区域划分,实际它和堆已经没有明显的物理划分,所以这里出现的内存溢出无法明确判断是哪一块的内存出现异常。
  • JDK1.8对JVM架构的改造将类元数据放到本地内存中,另外,将常量池和静态变量放到Java堆里。HotSpot VM将会为类的元数据明确分配和释放本地内存。在这种架构下,类元信息就突破了原来-XX:MaxPermSize的限制,现在可以使用更多的本地内存。这样就从一定程度上解决了原来在运行时生成大量类造成经常Full GC问题,如运行时使用反射、代理等。所以升级以后Java堆空间会增加,这也是jdk1.8环境中运行时不会内存溢出的原因。

4.本机直接内存溢出

本机直接内存溢出相关的虚拟机参数:
      -XX:MaxDirectMemorySize → 指定本机直接内存容量。如果不指定,默认与Java 堆最大值(-Xmx)一样。
实验:       

package com.test.ch2;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * 本机直接内存溢出实验
 * DirectMemory 容量可以通过 -XX:MaxDirectMemorySize 指定,如果不指定,则默认与java 堆最大值(-Xmx 指定)一样,下列代码越过了DirectByteBuffer类
 * 直接通过反射获取Unsafe实例进行内存分配(Unsafe 类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望rt.jar 中的类才能使用Unsafe的功能)
 */
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);
        }

    }
}

运行结果:
在这里插入图片描述
分析:

      由DirectMemory导致内存溢出,一个明显的特征是Heap Dump 文件中不会看到明显的异常,如果发现OOM之后Dump文件很小,而程序中直接或间接的使用了NIO那就可以考虑一下是不是直接内存溢出了。

总结

      虽然java 有垃圾回收机制,但是内存溢出异常离我们仍然不遥远,本文章通过各个实验讲解了各个区域出现内存溢出的原因以及一些明显的标志,可以帮助我们在出现内存溢出时快速定位到出现问题的地方。文章写了自己针对内存溢出的一些理解,有理解不对的地方,还请各位大神多多指教。

相关推荐
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页