一句话说透Java里面的内存区域和内存模型

234 阅读4分钟

一、内存区域:办公室功能区划分

比喻:JVM 内存就像一个公司的办公区,不同区域有不同用途

1. 堆(Heap)—— 大仓库

  • 作用:存放所有对象实例(所有员工领的办公用品都堆在这)

  • 特点

    • 线程共享(所有人随便拿)
    • 会抛出 OutOfMemoryError(仓库爆满)
  • 源码验证:HotSpot 的 CollectedHeap 类(如 genCollectedHeap.cpp

    // 堆内存分配核心逻辑(伪代码)
    HeapWord* GenCollectedHeap::mem_allocate(...) {
      if (尝试快速分配失败) {
       触发GC(); // 保洁阿姨清理仓库
       再次尝试分配;
      }
      if (仍失败) throw OutOfMemoryError;
    }
    

2. 虚拟机栈(Stack)—— 员工工位

  • 作用:存放方法调用时的栈帧(每个员工处理任务时的临时笔记本)

  • 特点

    • 线程私有(每人有自己的工位)
    • 会抛出 StackOverflowError(工位被笔记本堆满)
  • 栈帧结构

    | 局部变量表 | 操作数栈 | 动态链接 | 方法返回地址 |  
    
  • 源码对应frame.hpp 中的 frame 类(描述栈帧结构)

3. 方法区(Method Area)—— 档案室

  • 作用:存储类信息、常量、静态变量(公司规章制度、员工档案)

  • JDK 变化

    • JDK7:永久代(档案室在办公楼内)
    • JDK8+:元空间(Metaspace,档案室搬到云盘,用本地内存)
  • 源码线索metaspace.hpp 中的 Metaspace 类

    // 元空间内存分配(伪代码)
    void* Metaspace::allocate(size_t size) {
      if (当前块不够) {
        向操作系统申请新内存块; // 类似 malloc
      }
      return 分配地址;
    }
    

4. 程序计数器(PC Register)—— 任务进度条

  • 作用:记录当前线程执行到的字节码行号(员工知道自己做到哪一步了)
  • 唯一不会 OOM 的区域(老板必须知道每个员工的进度)

5. 本地方法栈(Native Method Stack)—— 外籍员工工位

  • 作用:执行 Native 方法(比如 C/C++ 写的功能)

6. 直接内存(Direct Memory)—— 外包仓库

  • 特点:通过 ByteBuffer.allocateDirect() 申请,不受 JVM 堆限制
  • 底层原理:调用 unsafe.allocateMemory() 直接分配系统内存

二、内存模型(JMM):办公室协作规则

比喻:JMM 是规范多线程如何安全传递数据的规则手册

1. 主内存 vs 工作内存

  • 主内存:公司公告板(所有线程可见)

  • 工作内存:员工私人笔记本(线程私有,存主内存的数据副本)

  • 数据同步问题

    复制

    线程A修改笔记本 → 未同步到公告板 → 线程B看到旧数据(脏读)  
    

2. 原子性/可见性/有序性

  • 原子性

    • synchronized 锁会议室(一次只允许一个线程操作)
    • 源码实现:objectMonitor.cpp 中的锁竞争逻辑
  • 可见性

    • volatile 强制要求员工修改笔记本后立刻抄到公告板

    • 底层原理:通过 CPU 的 MESI 协议 或 内存屏障 实现

      // HotSpot 的 volatile 写操作插入内存屏障
      OrderAccess::storeload(); // 写后加屏障,强制刷新
      
  • 有序性

    • 禁止指令重排序(员工必须按流程步骤办事)
    • happens-before 原则(公司规定的优先顺序)

3. 内存屏障(Memory Barrier)—— 行政监督

  • LoadLoad屏障:确保读操作顺序
  • StoreStore屏障:确保写操作顺序
  • LoadStore屏障:读不能重排到写之后
  • StoreLoad屏障:全能屏障(最严格)

4. synchronized 的锁升级

  • 无锁 → 偏向锁:第一个员工独占会议室(Mark Word 记录线程ID)

  • 偏向锁 → 轻量级锁:有竞争时升级为轮流使用(自旋等待)

  • 轻量级锁 → 重量级锁:竞争激烈时找行政协调(操作系统互斥量)

  • 源码证据markOop.hpp 中锁状态标记位

    enum { 
      locked_value             = 0, // 轻量锁
      unlocked_value           = 1, // 无锁
      monitor_value            = 2, // 重量锁
      marked_value             = 3, // GC标记
      biased_lock_pattern      = 5  // 偏向锁
    };
    

三、从源码看内存管理(HotSpot 核心逻辑)

1. 对象内存分配

  • 快速分配:TLAB(Thread Local Allocation Buffer)

    • 每个线程在堆里有一小块自留地
    • 源码:thread.cpp 中 ThreadLocalAllocBuffer 类

2. GC 触发条件

  • Young GC:Eden 区满时触发

    // GenCollectorPolicy 判断是否触发GC
    if (eden_space->used() > eden_space->capacity_in_bytes()) {
      collect_generation(...); // 开始打扫年轻代
    }
    
  • Full GC:老年代或元空间不足时触发

3. 内存溢出(OOM)真相

  • 堆溢出:创建大对象或内存泄漏(如循环引用未断开)
  • 栈溢出:递归调用无终止条件
  • 元空间溢出:加载过多类(比如动态生成类)

四、高频面试题解析

  1. String 存在哪里?

    • String s = "abc" → 常量池(方法区)
    • String s = new String("abc") → 堆 + 常量池
  2. volatile 和 synchronized 的区别?

    • volatile 是轻量级可见性控制
    • synchronized 是重量级原子性+可见性控制
  3. 为什么要有元空间替代永久代?

    • 避免永久代大小难预估导致 OOM
    • 元空间使用本地内存,上限由系统决定

五、总结口诀

「内存区域分六块,堆栈方法各不同
JMM管线程事,可见有序原子性
偏向轻量重量锁,屏障插入保顺序
元空间换永久代,直接内存更高效!」

附:常见内存相关参数

  • -Xmx:堆最大值
  • -Xss:栈大小
  • -XX:MetaspaceSize:元空间初始大小
  • -XX:MaxDirectMemorySize:直接内存上限