Java内存区域与内存溢出异样

运行时数据区域

程序计数器

  • 用于记录从内存执行的下一条指令的地址,线程公有的一小块内存,也是惟一不会报出OOM异样的区域

Java虚拟机栈

  • Java虚拟机栈(Java Virtual Machine Stack)是线程公有的,它的生命周期与线程雷同。虚拟机栈形容的是Java办法执行的线程内存模型:每个办法被执行的时候,Java虚拟机都会同步创立一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动静连贯、办法进口等信息。每一个办法被调用直至执行结束的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

  • 如果线程申请的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异样

  • 如果Java虚拟机栈容量能够动静扩大,当栈扩大时无奈申请到足够的内存会抛出OutOfMemoryError异样

本地办法栈

  • 与Java虚拟机栈相似,只不过服务对象不一样,本地办法栈为虚拟机应用到的本地办法服务,Java虚拟机栈为虚拟机执行Java办法(字节码)服务

Java堆

  • 对于Java应用程序来说,Java堆(Java Heap)是虚拟机所治理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创立。此内存区域的惟一目标就是寄存对象实例,Java世界里“简直”所有的对象实例都在这里分配内存
  • 当堆内存没有足够空间给对象实例分配内存并且堆内存无奈扩大时都会抛出OOM异样

办法区

  • 办法区与Java堆相似,也是各个线程共享的区域,它用于存储已被虚拟机加载的类型信息、常量、动态变量、即时编译器编译后的代码缓存等数据
  • 通常用别名“非堆”来与Java堆做辨别
  • 当办法区没有足够空间满足内存调配要求时,也会抛出OOM异样

运行时常量池

  • 运行时常量池是办法区的一部分,用于寄存编译期生成的各种字面量与符号援用
  • 受办法区内存限度,当常量池无奈再申请到内存时会抛出OOM异样

间接内存

  • 间接内存并不是运行时数据区的一部分,但它受总内存限度,也可能会呈现OOM异样

HotSpot虚拟机对象探秘

对象的创立

在类加载查看通过后,接下来虚拟机将为新生对象分配内存,而内存调配形式次要有两种:

  • 指针碰撞

  • 闲暇列表

对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局能够划分为三个局部:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

  • 对象头

    • 存储对象本身运行时数据(Mark Word),如哈希码(HashCode)、GC分代年龄、锁状态标记、线程持有的锁、偏差线程ID、偏差工夫戳等
    • 类型指针(对象指向其类型元数据的指针)
  • 实例数据

    • 对象真正存储的无效信息,即代码中的各类型字段内容
  • 对齐填充

    • 因为HotSpot虚拟机的主动内存管理系统要求对象起始地址必须是8字节的整数倍,即任何对象大小都是8字节的整数倍,故实例数据局部没有对齐的话须要对齐填充来充当占位符补全

对象的拜访定位

Java程序会通过栈上的reference(一个指向对象的援用)数据来操作堆上的具体对象,具体的拜访形式由虚拟机实现。

支流拜访形式次要有两种:

  • 句柄

  • 间接指针

实战OOM异样

采纳不同的JDK及垃圾回收收集器均可能会产生不同的后果,以下实战均以JDK8,ParallelGC垃圾收集器为例运行代码

# 查看默认垃圾收集器VM参数-XX:+PrintCommandLineFlags -version

Java堆溢出

只有一直创建对象实例,同时又防止垃圾收集器回收,这样达到最大堆容量限度后便能产生OOM异样

public class Hello {    /**     * -Xms:最小堆内存20M -Xmx:最大堆内存20M 两者设置一样防止主动扩大      * VM参数:-Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError     */    public static void main(String[] args) {        List<Hello> hellos = new ArrayList<>();        while (true) {            hellos.add(new Hello());        }    }}

Java虚拟机栈和本地办法栈溢出

《Java虚拟机标准》明确容许Java虚拟机实现自行抉择是否反对栈的动静扩大,而HotSpot虚拟机的抉择是不反对扩大,所以除非在创立线程申请内存时就因无奈取得足够内存而呈现OutOfMemoryError异样,否则在线程运行时是不会因为扩大而导致内存溢出的,只会因为栈容量无奈包容新的栈帧而导致StackOverflowError异样
  • 应用-Xss参数缩小栈容量
public class Hello {    /**     * VM参数:-Xss128k     */    private int stackLength = 1;    public void stackLeak() {        stackLength++;        // 递归调用办法,一直入栈        stackLeak();    }    public static void main(String[] args) throws Throwable {        Hello oom = new Hello();        try {            // 调用办法,入栈            oom.stackLeak();        } catch (Throwable e) {            System.out.println("stack length:" + oom.stackLength);            throw e;        }    }}

  • 定义了大量的本地变量,增大此办法帧中本地变量表的长度(即调整栈帧大小)
public class Hello {    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;        }    }}

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

  • 办法区容量管制
public class Hello {    /**     * JDK8前VM参数: -XX:PermSize=6M -XX:MaxPermSize=6M     * JDK8VM参数:-XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M     */    public static void main(String[] args) {        // 应用Set放弃常量池援用,防止Full GC回收常量池行为        Set<String> set = new HashSet<>();        // 在short范畴内足以让6M大小的PermSize(永恒代,JDK8前有,JDK8及之后版本都已采纳元空间代替)产生OOM了        short i = 0;        // JDK8前,抛出OOM异样        // JDK8下,失常状况会进入死循环,并不会抛出任何异样        while (true) {            // String.intern()进入字符串常量池            set.add(String.valueOf(i++).intern());        }    }}

上述代码在JDK8环境下并不会抛出任何异样,这是因为字符串常量池曾经被移至Java堆之中,管制办法区容量的大小对Java堆并没有什么影响

  • String.intern()办法介绍:如果字符串常量池中曾经蕴含一个等于此String对象的字符串,则返回常量池中这个字符串的String对象;否则,将此String对象蕴含的字符复制增加到常量池中,并返回此String对象的援用
/** * JDK6:false false * JDK8:true  false */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);}
  • JDK6因为new StringBuilder()调配到的是Java堆内存,而String.intern()会把首次遇到的字符串复制到的是字符串常量池(办法区),所以都是false

  • JDK8因为字符串常量池都挪动到了Java堆中,new StringBuilder()调配到Java堆内存后,字符串常量池也记录到了首次遇到的实例援用,那么String.intern()new StringBuilder()都是同一个了(true);而因为java字符串在sun.misc.Version类加载时已进入常量池,那么intern()办法就返回以后常量池的String对象,new StringBuilder()在堆中从新创立了一个,天然也就不一样了(false)

  • 办法区的主要职责是用于寄存类型的相干信息,如类名、拜访修饰符、常量池、字段形容、办法形容等,因而运行时产生大量的类填满办法区也能够造成办法区溢出
/* * 借助CGLib造成办法区溢出 * VM参数:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M */public class Hello {    public static void main(String[] args) {        while (true) {           // 创立CgLib加强对象            Enhancer enhancer = new Enhancer();            // 设置被代理的类            enhancer.setSuperclass(OOMObject.class);            enhancer.setUseCache(false);            // 指定拦截器            enhancer.setCallback(new MethodInterceptor() {                @Override                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {                    return proxy.invokeSuper(obj, args);                }            });            // 创立代理对象            enhancer.create();        }  }    static class OOMObject {    }}

本机间接内存溢出

间接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)统一

// 应用unsafe调配本机内存public class Hello {    // VM参数:-Xmx20M -XX:MaxDirectMemorySize=10M    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);        }    }}

参考资料

《深刻了解Java虚拟机》(第三版) 第2章:Java内存区域与内存溢出异样