内存区域与内存溢出异常

 2023-01-19
原文作者:小粥粥出击 原文地址:https://juejin.cn/post/6977960516572413988

0. 前言

版本: JDK7

1. 运行时数据区

202301011609398151.png

1.1 程序计数器

  • 较小的内存空间,当前线程所执行的字节码的行号指示器
  • 线程执行 Java 方法,记录正在执行的虚拟机字节码指令的地址
  • 线程执行 Native 方法,记录为空(Undefined)
  • 唯一没有规定 OOM 情况的区域

1.2 虚拟机栈(Java 栈)

  • 描述 Java 方法执行的内存模型

  • 每个方法执行时创建一个栈帧,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息

    • 进入方法栈帧入栈、离开方法栈帧出栈
    • 局部变量表存储基本数据类型(byte、short、int、long、float、double、char、boolean)、对象引用(reference 类型,指向对象的起始地址的引用指针、指向代表对象的句柄地址、指向其他与对象相关的位置)、returnAddress 类型(指向字节码指令的地址)
    • 局部变量表内存大小编译期确定,运行期不改变
  • 线程请求栈深度大于虚拟机允许深度,抛出 SOF 异常

  • 栈扩展时无法申请足够内存,抛出 OOM 异常

1.3 本地方法栈

  • 类似虚拟机栈,服务于 Native 方法
  • 对本地方法栈中方法使用的语言、方式和数据结构无强制规定,可由不同虚拟机自由实现
  • 类似虚拟机栈可抛出 SOF 和 OOM 异常

1.4 堆

  • 虚拟机启动时创建,大多数虚拟机中内存最大的区域
  • 几乎所有对象实例在此分配内存
  • 垃圾收集器主要管理的区域
  • 可以处于物理上的不连续内存空间,只要逻辑上连续即可
  • 没有内存空间为对象实例分配内存,抛出 OOM 异常

1.5 方法区

  • 存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • HotSpot 虚拟机使用永久代实现该区域,但仍然需要垃圾管理器回收内存,回收条件苛刻
  • 可以处于物理上的不连续内存空间,可以选择不实现垃圾回收
  • 无法满足内存分配时,抛出 OOM 异常

1.6 运行时常量池

  • 存储编译期生成的字面量和符号引用

    • 字面量:字符串文本、8种基本数据类型的值、声明为 final 的常量
    • 符号引用:类和接口的全限定名、字段名和描述符、方法名和描述符
  • 运行期可将新的常量放入该区域

  • 无法满足内存分配时,抛出 OOM 异常

1.7 字符串常量池

  • 存储不重复字符串
  • String str = "hello"; 字符串常量池存在 "hello" 时返回引用给变量,不存在时创建一个再返回引用给变量
  • String str = new String("hello"); 字符串常量池存在 "hello" 时不创建,不存在时创建一个;同时 new 关键字还会在堆中创建对象实例,返回引用给变量

2. HotSpot 虚拟机对象

2.1 对象创建

类加载 -> 分配内存空间 -> 内存空间初始化零值(不包括对象头) -> 设置对象信息(例如对象头) -> 执行 init 方法,按照程序员意愿初始化

2.1.1 内存分配方式

堆的垃圾收集器是否带压缩整理功能决定了内存是否规整,内存是否规整决定了分配方式

  • 指针碰撞:如果堆中内存是规整的,已分配和未分配的区域各放一边,中间存在一个指针作为分界点的指示器。为对象分配内存时,指针向未分配区域移动对象内存大小相等的距离

    202301011609403232.png

  • 空闲列表:如果堆中内存不规整,已分配和未分配区域互相交错,则虚拟机需要维护一个列表,记录哪块内存区域可用。为对象分配内存时,从列表中找出一块可用区域,并更新列表上的记录

    202301011609407653.png

2.1.2 内存分配时线程安全问题

虚拟机频繁进行内存分配时,因为堆是线程共享的,所以存在线程安全问题,解决问题方式如下:

  • CAS 配合失败重试,保证操作的原子性

  • 本地线程分配缓冲(TLAB):每个线程在堆中预先分配一块区域,各个线程在各自的区域分配内存。只有 TLAB 用完需要分配新 TLAB 时,才需要同步锁定(可通过 -XX:+/-UseTLAB 参数设定)

    202301011609412084.png

2.2 对象内存布局

202301011609416835.png

  • 对象头:

    • 标记字(Mark Word):

      • 存储对象运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有锁、偏向线程 ID、偏向时间戳等
      • 非固定数据结构,以便在极小空间内存储更多信息
    • 类型指针:

      • 指向该对象的类元数据的指针,虚拟机可以通过该指针确认对象属于哪个类的实例
    • 数组长度:

      • 数组对象时,记录数组长度
  • 实例数据:

    • 存储对象真正有效信息
    • 存储顺序受虚拟机分配策略和字段在 Java 源码中定义的顺序影响
  • 对齐填充:

    • 占位符作用,保证对象大小为8字节的整数倍

2.3 对象的访问定位

当需要使用对象时,可以通过栈中的 reference 数据操作堆中的具体对象。reference 数据保存了指向对象的引用,通过引用访问对象的主流方式有使用句柄和直接指针两种

  • 使用句柄:堆划分出一块区域作为句柄池,栈中的 reference 存储的是对象句柄地址,句柄中包含了对象的实例数据和类型数据的具体地址

    202301011609422806.png

  • 直接指针:栈中的 reference 存储的是对象的在堆中的地址,堆中的对象布局放置对象实例数据和类型数据的具体地址

202301011609430517.png

两种方式各有优势:

  • 使用句柄:栈中的 reference 存储的对象句柄地址是稳定的,当对象被移动时(垃圾回收时的普遍行为)只用改变句柄中的实例数据指针
  • 直接使用:访问速度快,节省了一次指针定位的时间开销,HotSpot 虚拟机使用该方式

3. 数据区内存溢出实战

3.1 堆溢出

不断 new 对象,导致堆无法分配内存给对象抛 OOM 异常

    /**
     * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
     
     * -Xms 堆内存初始值
     * -Xmx	堆内存最大值
     * -XX:+HeapDumpOnOutOfMemoryError 内存溢出时 dump 出当前内存堆转储快照,便于后续分析
     */
    public class HeapOOM {
    
        static class OOMObject {
        }
    
        public static void main(String[] args) {
            List<OOMObject> list = new ArrayList<OOMObject>();
    
            while (true) {
                list.add(new OOMObject());
            }
        }
    }

在线工具分析内存堆转储快照:PerfMa

3.2 栈溢出

递归不断调用方法新增栈帧,栈无法分配内存给栈帧抛 SOF 异常

    /**
     * VM Args:-Xss256k
     *
     * -Xss 栈内存大小
     */
    public class StackSOF1 {
    
        private int stackLength = 1;
    
        public void stackLeak() {
            stackLength++;
            stackLeak();
        }
    
        public static void main(String[] args) throws Throwable {
            StackSOF1 oom = new StackSOF1();
            try {
                oom.stackLeak();
            } catch (Throwable e) {
                System.out.println("stack length:" + oom.stackLength);
                throw e;
            }
        }
    }

不断新增线程获取栈内存,导致进程中的内存无法分配内存给栈内存抛 OOM 异常

    /**
     * 谨慎尝试,随时死机
     * VM Args:-Xss50M
     *
     * -Xss 栈内存大小
     */
    public class StackOOM {
    
        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 {
            StackOOM oom = new StackOOM();
            oom.stackLeakByThread();
        }
    }

3.3 方法区溢出

通过 Cglib 不断创建代理类,方法区无法分配内存给类信息抛出 OOM 异常

    /**
     * VM Args: -XX:PermSize=2M -XX:MaxPermSize=2M
     *
     * -XX:PermSize 持久化内存初始值
     * -XX:MaxPermSize 持久化内存最大值
     */
    public class MethodAreaOOM {
    
        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 {
        }
    }

3.4 直接内存溢出

unsafe.allocateMemory() 申请内存,直接内存无法分配内存抛出 OOM 异常

    /**
     * VM Args:-XX:MaxDirectMemorySize=200M
     *
     * -XX:MaxDirectMemorySize 直接内存最大值
     */
    public class DirectMemoryOOM {
    
        private static final int _100MB = 1024 * 1024 * 100;
    
        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(_100MB);
            }
        }
    }

学自《深入理解Java虚拟机》(第二版)