大家好,我是程序员二叉。


简介

本文整理JVM运行时内存分区全套面试考点,包含内存区域划分、String s = new String("abc")内存分配流程、堆分代设计原理、Survivor两区作用、方法区与元空间对比、永久代淘汰原因、直接内存特性等高频问题。欢迎点赞收藏关注。


一、JVM运行时内存模型分区

JVM运行时数据区分为线程私有线程共享两大块,直接决定内存分配与GC范围。

1. 线程私有(每个线程独立,线程销毁内存释放)

  1. 程序计数器
    记录线程当前执行的字节码指令行号,唯一一个不会发生OOM的内存区域。
  2. 虚拟机栈
    每个方法执行时创建栈帧,存放局部变量表、操作数栈、动态链接、方法返回地址;局部变量包含基本数据类型、对象引用地址。栈深度超限抛出StackOverflowError
  3. 本地方法栈
    专门为JNI native本地方法提供运行内存,结构与虚拟机栈一致。

2. 线程共享(JVM启动分配,全局共用)

  1. Java堆(Heap)
    所有对象实例、数组的唯一分配区域,GC回收的核心区域,可通过-Xmx/-Xms调节最大最小堆内存。
  2. 方法区(JDK8后实现为元空间Metaspace)
    存储类元数据、静态变量、运行时常量池、JIT编译缓存等。
  3. 运行时常量池
    属于方法区的子区域,存放编译期字面量、符号引用,运行期也可动态放入常量。

补充:直接内存

不属于JVM运行时数据区,是操作系统物理本地内存,NIO框架大量使用。


二、String s = new String(“abc”) 各部件对应内存区域

  1. 类加载阶段:字符串字面量"abc"存入方法区的运行时常量池
  2. 执行new String():在Java堆中创建一个String实例对象
  3. 虚拟机栈的局部变量表生成引用变量s,栈内只存地址
  4. 堆内String对象内部的value字符数组,指向常量池里的"abc"

内存归属总结:

  • 虚拟机栈:引用变量s
  • Java堆:new出来的String实例对象
  • 方法区:常量字符串"abc"

三、堆为什么划分新生代、老年代?默认比例

1. 分代设计的原因

  1. 对象生命周期两极分化:绝大多数对象朝生夕死,少量对象长期存活
  2. 适配不同GC算法提升性能:
    • 新生代存活对象少,使用复制算法,GC速度极快
    • 老年代存活对象多,使用标记清除/标记整理,减少复制开销
  3. 避免每次GC扫描整个堆,降低全局停顿时间

2. 默认内存占比

新生代 : 老年代 = 1 : 2
新生代占整个堆内存1/3,老年代占2/3。


四、Eden、From、To Survivor作用;为什么设置两块Survivor

1. 各区基础作用

  1. Eden区:所有新创建对象优先分配在此,占新生代80%
  2. From(S0) Survivor:上一次Minor GC筛选出的存活对象存放区
  3. To(S1) Survivor:本次Minor GC的存活对象复制目标空白区

新生代内部默认比例:Eden : From : To = 8 : 1 : 1

2. 必须两个Survivor的核心原因

采用复制回收算法,解决内存碎片问题:

  1. 同一时间永远只用Eden + 一块Survivor,另一块保持空白
  2. Minor GC时,Eden+From存活对象全部拷贝到空白To区
  3. 拷贝完成直接清空Eden和From,互换From/To标识
  4. 全程不会产生内存碎片,回收效率远高于单Survivor

如果只设置一块Survivor,存活对象挪走后会产生大量零散空闲块,内存碎片化严重。


五、方法区 / 元空间 作用与存储内容

1. 核心作用

线程共享内存区域,用于存储所有被类加载器加载后的类元信息,支撑类实例化、方法调用、静态变量访问。

2. 存放内容

  • 类基础元数据:类名、父类、接口、访问修饰符、字段/方法描述
  • static静态变量、静态常量
  • 运行时常量池(字符串常量、数字常量、符号引用)
  • 构造方法、普通方法字节码
  • JIT即时编译器编译后的本地机器码缓存

六、永久代(PermGen)和元空间(Metaspace)区别;JDK8废弃永久代原因

1. 对比表格

对比项 永久代(JDK7及之前) 元空间(JDK8+)
内存位置 堆内存内部划分 操作系统本地物理内存
大小限制 固定值,必须配置-XX:MaxPermSize 默认无上限,仅受整机物理内存约束
OOM概率 极易出现PermGen OOM 大幅降低内存溢出风险
GC机制 Full GC才会回收类元数据,回收滞后 自动阈值扩容,垃圾回收更及时

2. JDK8废弃永久代改用元空间的理由

  1. 永久代大小固定,动态加载大量类(Spring、MyBatis、动态代理)极易触发OOM
  2. 永久代调参门槛高,运维很难预估所需内存大小
  3. HotSpot永久代与JRockit、IBM J9虚拟机内存模型不统一,元空间统一架构
  4. 把类元数据移到本地内存,堆内存压力降低,GC压力减小
  5. 简化内存模型,移除永久代相关复杂逻辑

七、直接内存是什么?是否会发生OOM

1. 直接内存定义

  1. 不属于JVM堆、不属于运行时五大内存区,直接向操作系统申请的物理内存
  2. NIO中ByteBuffer.allocateDirect()分配使用,零拷贝优化IO传输性能
  3. 读写数据减少用户态与内核态之间的内存拷贝,速度优于堆内缓冲区

2. 会不会OOM?

会出现直接内存溢出

  1. 不受-Xmx堆最大值约束,但整机物理内存有上限
  2. 大量分配DirectBuffer且不手动释放,内存持续占用耗尽系统内存,抛出OOM
  3. 可通过启动参数-XX:MaxDirectMemorySize手动限制直接内存最大容量

总结

  1. JVM内存分线程私有(程序计数器、虚拟机栈、本地方法栈)+ 共享(堆、方法区)+ 直接内存
  2. new字符串:栈存引用、堆存对象、方法区存常量字面量
  3. 堆分代是为匹配对象生命周期优化GC效率,新老默认1:2
  4. 双Survivor依靠复制算法消除内存碎片,新生代默认8:1:1
  5. 方法区存类元数据、静态变量、常量池;JDK8元空间替换堆内永久代
  6. 直接内存提升IO速度,无堆上限但物理内存有限,依然会OOM
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐