Java JVM 内存区域与内存溢出异常

时间:2022-04-13 20:56:16

运行时数据区域

Java JVM 内存区域与内存溢出异常

1.程序计数器

字节码的行号指示器,分支,循环,太哦转,异常,线程恢复都需要这个,CPU中上下文切换就涉及到线程恢复

是唯一一个在JVM中没有规定OutOfMemoryError的情况

2.Java虚拟机栈

生命周期和线程相同,描述Java方法执行时的内存模型,包括局部变量表,操作数栈,动态链接,方法出口等

局部变量存放基本类型,对象引用(对象的指针),returnAddress

long和double会占用两个局部变量空间(Slot)

栈深大于虚拟机要求,*;若可动态扩展,申请不到足够内存,OutOfMemoryError(OOM)

3.本地方法栈 Native Method Stack

native是与C++联合开发的时候用的!

使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用

。 这些函数的实现体在DLL中,JDK的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。总而言之:

  1. native 是用做java 和其他语言(如c++)进行协作时使用的,也就是native 后的函数的实现不是用java写的。
  2. 既然都不是java,那就别管它的源代码了,我们只需要知道这个方法已经被实现即可。
  3. native的意思就是通知操作系统, 这个函数你必须给我实现,因为我要使用。 所以native关键字的函数都是操作系统实现的, java只能调用。
  4. java是跨平台的语言,既然是跨了平台,所付出的代价就是牺牲一些对底层的控制,而java要实现对底层的控制,就需要一些其他语言的帮助,这个就是native的作用了

具体和java虚拟机栈类似,会OOM

4.Java堆

堆是被所有线程共享的内存区域,主要就是负责对象示例的空间分配,以及垃圾回收,垃圾回收分代但是归根结底是对象。

通过-Xmx和-Xms控制内存的扩展,不需要连续的内存,不够也会OutOfMemortError

5. 方法区

用于储存类信息,常量,静态变量,即时编译器编译后的代码等数据。被描述成堆的一个逻辑部分,但是有个‘非堆’的别名,会OOm

6.运行时常量池

是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,具备动态性,运行期间也可能将新的常量放入。

也包括字符串常量,例子 String类的intern()方法,会OOM

7.直接内存

nio,New input/output 直接内存,基于Channel和Buffer方式,在一些场景中避免在Java堆和Native堆中来回复制数据

也会OOm

OOM实例

1.Java堆溢出

-Xms20m -Xmx20m 表示最小值和最大值都是20M,不可扩展

-XX:+HeapDumpOnOutOfMemoryError 内存异常时Dump出快照进行分析

/** * 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());
        }
    }
}

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

-Xss128k 设置栈内存容量

//单线程*
/** * 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;
        }
    }
}

多线程OOM,此时不断申请线程,因为每个线程有自己的栈,所以导致系统内存减去堆内存,方法区内存,如果每个线程要求很大的内存,能申请的线程就少了,OOM。只能通过减小堆最大内存和栈容量来处理。

/** * VM Args:-Xss2M (这时候不妨设大些) * @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();
       }
}

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

运行时常量池时方法区的一部分,String#intern方法中看到,这个方法是一个 native 的方法。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。

JDK 1.6 以前常量池在永久代PermSize,1.7以后在Java Heap区,所以下面的代码在1.6中会OOM

/** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * @author zzm */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        // 使用List保持着常量池引用,避免Full GC回收常量池行为
        List<String> list = new ArrayList<String>();
        // 10MB的PermSize在integer范围内足够产生OOM了
        int i = 0; 
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

一个有趣的例子

//jdk 1.6 false false string.intern在永久代,StringBuilder在Java堆
//jdk 1.7 true false(String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。但是"java"字符串不是首次出现)
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        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);
    }   }
}

方法区用来存放Class相关的信息,如类名,访问修饰符,常量池,字段描述,方法描述。用CGLib生成动态类。方法区的OOM比较常见,类被回收判定比较苛刻。

/** * 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 {

    }
}

4. 本机直接内存溢出

DirectMemory容量-XX:MaxDirectMemorySize指定,如果不指定,默认Java堆最大值-Xmx

/** * 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);
        }
    }
}

DM导致内存溢出的一个明显特征是Dump文件很小。