java 内存区域划分与内存溢出异常(浅谈解决OutOfMemoryError)

本文章根据周志明写的深入理解jvm第二章,以及个人的理解,查阅的资料而编写

以下内容不做说明,jdk指1.6,虚拟机指 hotspot

运行时数据区

根据《java虚拟机规范 SE7版》 规定,java虚拟机所管理的内存将会包括一下几个运行时数据区

1.程序计数器

    程序计数器是一块较小的内存区域,用于当前执行程序的字节码的行号指示,如果java在执行 native 方法的时候,则这个计数器为空(undefined),该区域是唯一一个在 jvm虚拟机中没有规定任何 OOM 的区域。该区域为线程私有

2.java虚拟机栈

   与程序计数器一样,该区域也是线程私有的,它的生命周期与线程相同,其包含局部变量表部分,局部变量表里面存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,long,double,float),对象的引用(reference类型,可能是指向对象地址的指定,也可能是指向一个代表对象的句柄或其他相关的对象位置)和 returnAddress 类型。

  该区域规定了两个异常,StackOverflow 和 OOM,该区域可以手动设置大小,通过 -Xss 设置栈大小

3.本地方法栈

   该区域作用与java虚拟机栈很像,只不过是用来为本地方法执行提供服务。HotSpot 虚拟机就将 java虚拟机栈和本地方法栈 合二为一了。该区域一样规定有  StackOverflow 和 OOM

4.java堆

   一般来说,Java堆是java虚拟机中锁管理的内存中最大的一块。java堆是被所有线程所共享的,在虚拟机启动的时候创建。此区域的唯一目标就是存放对象的实例,几乎所有的对象是你都要在这里分配内存。(jvm规范规定),但是随着 JIT 编译器的发展与逃逸分析技术的逐渐成俗,栈上分配,标量替换优化技术将会导致一些微妙的变化,所以所有的对象都分配在堆上也不是那么绝对了。

   java堆是gc管理的主要区域,从内存回收的角度来看,由于现在很多收集器基本上都采用分代收集算法,所以java堆可以细分为:新生代和老年代;从内存分配的角度来看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区。

   不论如何划分,都与存放的内容无关,无论哪个区域,存储的依然都是对象的实例,进一步的划分只是为了更好的回收内存。该区域规定有 OOM 异常,该区域可以手动设置大小(-Xms 设置最小堆内存,-Xmx设置最大堆内存)。

5.方法区

  该区域在1.8之后被元数据区域(放在本地内存中,和直接内存一样)取代,所存储的数据也被其他的区域瓜分。

  该区域存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。在hotspot 虚拟机上,该区域被称之为永久代。jdk1.7之前可以使用 -XX:PermSize=xx 和 -XX:MaxPermSize=xx 来设置最小和最大内存。该区域规定有 OOM 

  设置元数据区域大小:-XX:MetaspaceSize=8m  -XX:MaxMetaspaceSize=50m

6.运行时常量池

  该区域在jdk1.8之后被存放到了堆中。

  该区域是方法区的一部分,用于存储类的版本,字段,方法,接口等描述信息,还有常量池,用于存储编译期生成的各种字面量和符号引用。java语言不要求常量一定只有编译期才能产生,运行期间也可以将新的常量放入池中,比如说 String.intern() 方法将String 方法常量池。

7.直接内存

  java1.4之后引入了 NIO,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在java堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。避免了在 java堆和 Navite堆中来回复制数据。提高了性能。

实战OOM异常

以下例子为 jdk1.8,hotspot 虚拟机

1.堆溢出

设置运行参数: -Xms20m 0Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:// (指定打印出来的 dump 文件的路径)

import java.util.ArrayList;

/**
 * -Xms20m 0Xmx20m -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=D:// (指定打印出来的 dump 文件的路径)
 * 设置 最小堆内存为20m,最大也为 20m,打印 oom 时候的内存堆转储快照
 */
public class HeapOOM {

    static class OOMObject{}

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

运行后控制台截图

使用 java自带的 visualVM 打开 .hprof 文件,

可以看到出现问题的线程 main

点击之后,可以看到出现问题的位置

点击上面的 类 可以发现,该oom 就是因为有过多的 OOMObject 才导致的

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

对于 hotspot来说,因为虚拟机栈和本地方法栈合二为一了,所以设置本地方法栈大小的 -Xoss 参数是没有效果的。栈容量只由    -Xss 来设置。关于虚拟机栈和本地方法栈,在java虚拟机规范中描述了两种异常,

    1.请求深度超过虚拟机所允许的最大深度,则抛出 StackOverflowError (例如不结束的 递归)

    2.如果虚拟机在扩展的时候无法申请到足够的内存,则 OOM

-Xss128k 设置虚拟机栈内存为 128k

/**
 * -Xss128k 设置虚拟机栈内存为 128k
 */
public class StackOverflow {

    int stackLength = 0;

    public void stackLeak(){
        this.stackLength ++;
        stackLeak();
    }

    public static void main(String[] args) {
        StackOverflow overflow = new StackOverflow();
        try {
            overflow.stackLeak();
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println("stack length is " + overflow.stackLength);
            //throw t;
        }
    }
}

3.运行时常量池OOM


由于jdk1.8之后运行时常量池被划分到了 堆中,所以此处只需要将堆内存设置小,就可以轻松的看到错误

/**
 * 运行时常量池 oom ,该区属于方法区,jdk1.6之前属于永久区,在jdk1.7之后开始去永久代,所以该代码在 1.7之后就不会达到预期的效果了
 * -XX:PermSize=10m -XX:MaxPermSize=10m
 * jdk1.8 之后该区域属于堆,所以改变该区域大小通过改变堆就可以达到
 */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        int i = 0;
        while (true){
            list.add( String.valueOf(i+++"========================================================================================================================================================================================================================================================================================================================================================================================================================>").intern() );
        }

//        String str1 = new StringBuffer("计算机").append("软件").toString();
//        System.out.println( str1.intern() == str1 );
//
//        String str2 = new StringBuffer("ja").append("va").toString();
//        System.out.println( str1.intern() == str1 );
    }

}
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to java_pid108728.hprof ...
Exception in thread "main" Heap dump file created [21150118 bytes in 0.059 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded

4.本机直接内存溢出

  DirectMemory 容量可以通过 -XX:MaxDirectMemorySize 指定,如果不指定,则默认与java堆最大值一样。

/**
 * 直接内存 oom
 * 直接内存,独立于运行时数据区之外,是 nio 使用的
 * DirectMemory 容量可以通过 -XX:MaxDirectMemorySize 指定,如果不指定,默认与Java堆最大值(-Xmx指定)一样
 * -Xmx10m -XX:MaxDirectMemorySize=10m
 *
 * 由DirectMemory 导致的oom 一个明显的特征就是在 Heap Dump 文件中看不到明显的异常 ,而且 dump 文件还非常小
 */
public class DirectMemoryOOM {
    private static final int _1m = 1024*1024;

    public static void main(String[] args) throws IllegalAccessException {
        Field field = Unsafe.class.getDeclaredFields()[0];
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        while (true){
            unsafe.allocateMemory(_1m);
        }
    }
}

由DirectMemory导致的内存溢出,一个明显的特征就是在 HeapDump 文件中不会看见明显的异常,而且 dump文件还很小。

 

 

JDK1.8 JVM运行时数据区域划分参考:https://blog.csdn.net/bruce128/article/details/79357870

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值