JVM内存模型大纲

198 阅读4分钟

JVM内存模型概述

JVM内存模型是Java程序运行时的数据存储结构,分为 线程私有区域 和 线程共享区域

  • 线程私有:程序计数器、虚拟机栈、本地方法栈。
  • 线程共享:堆、方法区(JDK8后称为元空间)。

堆(Heap)

1. 核心作用

  • 存储所有对象实例和数组(new关键字创建的对象)。
  • 垃圾回收的主要区域(GC的重点目标)。

2. 堆的结构

堆分为 新生代(Young Generation)  和 老年代(Old Generation) ,默认比例 1:2

  • 新生代

    • Eden区(伊甸园):对象初次分配的区域。
    • Survivor区(存活区):分为 From 和 To,用于Minor GC后存活对象的暂存。
    • 默认比例 Eden:From:To = 8:1:1
  • 老年代:存放长期存活的对象(经过多次Minor GC仍存活的对象)。

新生代中,Eden区到Survivor区只需要经过一次GC,而由新生代到老年代可以通过**-XX:MaxTenuringThreshold** jvm命令设置,如果没有设置的话,默认是15,也就是经过15次GC,但是这个15并非是绝对的,它只是一个最大阈值,当Survivor区出现内存不足的情况下,对象也会提前划到老年代,所以总结下来这个15指的是对象小于等于15次GC会被划分到老年代,不存在超过15次GC的情况出现

堆的GC机制

  • Minor GC:清理新生代,触发条件为Eden区满。
  • Major GC/Full GC:清理老年代或整个堆,触发条件包括老年代空间不足、方法区空间不足等。

关键问题与调优

  • OOM(OutOfMemoryError)

    • 原因:堆内存不足(对象过多或内存泄漏)。
    • 解决:调整堆大小(-Xmx-Xms),优化对象生命周期。
  • 调优参数

    -Xmx4g  # 最大堆内存
    -Xms4g  # 初始堆内存
    -XX:NewRatio=2  # 新生代与老年代比例
    -XX:SurvivorRatio=8  # Eden与Survivor区的比例
    

    这里常见的调优方式就是把最大堆内存和最小堆内存设置为同等大小,这样做的好处就是在程序启动时就获得最佳的内存,稳定运行

    虚拟机栈(Stack)

    1. 核心作用

  • 存储 方法调用的栈帧,每个方法对应一个栈帧,包含:

    • 局部变量表(基本类型、对象引用)。
    • 操作数栈(方法执行中的中间计算结果)。
    • 动态链接(指向方法区的方法引用)。
    • 方法返回地址。

2. 栈的异常

  • StackOverflowError

    • 原因:栈深度超过限制(如无限递归)。
    public void infiniteRecursion() {
        infiniteRecursion();  // 递归调用导致栈溢出
    }
    
  • OOM(OutOfMemoryError)

    • 原因:线程过多导致栈总内存不足(较少见)。

3. 调优参数

-Xss256k  # 每个线程的栈大小(默认1MB)

方法区(Method Area)

1. 核心作用

  • 存储 类信息(类名、父类、接口、字段、方法等)。
  • 运行时常量池(字符串字面量、符号引用)。
  • 静态变量(JDK7前静态变量在方法区,JDK7后移至堆中)。

2. 方法区的实现

  • JDK7及之前:称为 永久代(PermGen) ,存在OOM风险。
  • JDK8及之后:改为 元空间(Metaspace) ,使用本地内存(Native Memory)。

3. 元空间的关键问题

  • OOM

    • 原因:加载的类过多(如动态生成类未释放)。
    • 解决:调整元空间大小。
  • 调优参数

    -XX:MetaspaceSize=128m  # 初始元空间大小
    -XX:MaxMetaspaceSize=256m  # 最大元空间大小
    

    内存模型对比表

区域线程共享/私有存储内容异常类型调优参数示例
共享对象实例、数组OutOfMemoryError-Xmx-Xms
私有栈帧(局部变量、操作数栈)StackOverflowError-Xss
方法区共享类信息、运行时常量池OutOfMemoryError-XX:MetaspaceSize

实战问题与解决方案

1. 内存泄漏排查

  • 场景:堆内存持续增长,Full GC无法回收。

  • 工具

    • jmap -histo:live <pid>:查看堆中对象分布。
    • MAT(Memory Analyzer Tool):分析堆转储文件(.hprof)。
  • 常见原因:未关闭的资源(如数据库连接)、静态集合长期持有对象。

2. 栈溢出调试

  • 场景:递归调用导致StackOverflowError
  • 解决:限制递归深度或改用循环。

3. 元空间OOM优化

  • 场景:动态生成类(如CGLib代理)未释放。
  • 解决:增加MaxMetaspaceSize,或优化类加载逻辑。

高频面试题

  1. 堆和栈的区别是什么?

    • 堆存储对象,线程共享,GC管理;栈存储方法调用,线程私有,自动释放。
  2. 如何定位JVM内存泄漏?

    • 使用jmap生成堆转储文件,通过MAT分析对象引用链。
  3. JDK8为什么用元空间替代永久代?

    • 避免永久代固定大小导致的OOM,元空间使用本地内存,可动态扩展。