乐趣区

关于java:深入理解Java虚拟机之Java内存区域与内存溢出异常

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 内存区域与内存溢出异样

退出移动版