1-1-1 Java深度强化-JVM内存模型与GC机制

34 阅读7分钟

好的,这是一个非常核心的Java面试和性能调优话题。我将系统地为你讲解JVM内存模型(通常指运行时数据区)和垃圾回收机制,并阐明它们之间的联系。


一、JVM内存模型(运行时数据区)

JVM在执行Java程序时,会把它管理的内存划分为若干个不同的数据区域。这些区域有不同的用途、创建和销毁时间。

核心区域可以分为两大类:线程共享的和线程私有的。

1. 线程共享的区域(所有线程共享同一份)

    • 作用:存放所有对象实例数组。是GC管理的主要区域,因此也叫“GC堆”。
    • 特点
      • 在JVM启动时创建,是内存中最大的一块。
      • 为了GC效率,现代JVM堆内存通常进一步划分为:
        • 年轻代:新创建的对象首先放在这里。分为一个Eden区和两个Survivor区。
        • 老年代:在年轻代中经历多次GC后仍存活的对象,会被晋升到这里。
        • 元空间:JDK8+ 替代了永久代,存放类元信息、方法区数据、常量池等。使用本地内存,理论上只受操作系统限制。
    • 异常OutOfMemoryError(当堆无法扩展时抛出)。
  • 方法区

    • 作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
    • 具体实现
      • JDK7及以前:称为“永久代”,是堆的一部分。
      • JDK8及以后:称为“元空间”,使用本地内存,不再属于堆。
    • 运行时常量池:是方法区的一部分,存放编译期生成的各种字面量符号引用
    • 异常OutOfMemoryError

2. 线程私有的区域(每个线程独有一份)

  • 程序计数器

    • 作用:指向当前线程正在执行的字节码指令的地址。是控制流程(分支、循环、跳转、异常处理)的基础。
    • 唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。
  • 虚拟机栈

    • 作用:描述Java方法执行的内存模型。每个方法执行时,JVM会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    • 栈帧:方法从调用到执行完成,对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
    • 异常
      • StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度(如无限递归)。
      • OutOfMemoryError:如果栈可以动态扩展,但扩展时无法申请到足够内存。
  • 本地方法栈

    • 作用:与虚拟机栈类似,区别在于它为JVM使用的本地方法服务。
    • 异常:同样会抛出StackOverflowErrorOutOfMemoryError

二、垃圾回收机制

GC负责自动管理堆内存,回收不再使用的对象占用的空间。其核心是解决三个问题:

  1. 哪些内存需要回收?(对象存活判定)
  2. 什么时候回收?
  3. 如何回收?(GC算法)

1. 对象存活判定

  • 引用计数法:给对象添加一个引用计数器,有引用则+1,引用失效则-1。为0时回收。无法解决循环引用问题,Java主流JVM未采用。
  • 可达性分析算法:Java主要使用的方法。
    • 通过一系列称为 “GC Roots” 的对象作为起点,向下搜索,走过的路径称为“引用链”。如果一个对象到GC Roots没有任何引用链相连,则证明此对象不可用。
    • GC Roots 包括
      • 虚拟机栈中引用的对象(局部变量)。
      • 本地方法栈中JNI引用的对象。
      • 方法区中静态属性引用的对象(静态变量)。
      • 方法区中常量引用的对象(如字符串常量池里的引用)。
      • 所有被同步锁持有的对象。
      • JVM内部引用(如类加载器、基本类型对应的Class对象)。

2. GC算法(如何回收)

  • 标记-清除
    • 过程:先标记所有需要回收的对象,标记完成后统一回收。
    • 缺点:产生内存碎片,效率一般。
  • 标记-复制
    • 过程:将内存分为两块,每次只使用一块。GC时,将存活对象复制到另一块,然后清空已使用块。
    • 优点:无碎片,高效。
    • 缺点:内存利用率只有50%。常用于年轻代的Survivor区
  • 标记-整理
    • 过程:标记所有存活对象,然后将它们向内存空间的一端移动,然后清理掉边界以外的内存。
    • 优点:无内存碎片
    • 缺点:移动对象成本高。常用于老年代

3. 分代收集理论(现代JVM GC的基础)

根据对象存活周期的不同,将堆内存划分为年轻代和老年代,从而采用最合适的收集算法。

  • 年轻代:对象“朝生夕死”,回收频繁。
    • 算法:使用标记-复制算法,因为存活对象少,复制成本低。
    • 过程:新对象在Eden区创建。当Eden满时,触发 Minor GC。将Eden和其中一个Survivor中存活的对象复制到另一个Survivor区,同时对象年龄+1。当对象年龄达到阈值(默认15),则晋升到老年代
  • 老年代:对象存活时间长。
    • 算法:使用标记-清除标记-整理算法。
    • 触发条件:老年代空间不足时,触发 Full GC(通常会伴随一次Minor GC,对整堆进行回收,速度慢)。
  • 元空间:回收类元信息、废弃的常量和类加载器。

4. 常见垃圾收集器(GC算法的实现)

  • 年轻代收集器
    • Serial:单线程,STW(Stop-The-World,暂停所有应用线程),简单高效,适合Client模式。
    • ParNew:Serial的多线程版本,需与CMS配合。
    • Parallel Scavenge:多线程,目标是达到可控制的吞吐量
  • 老年代收集器
    • Serial Old:Serial的老年代版本。
    • Parallel Old:Parallel Scavenge的老年代版本,注重吞吐量。
    • CMS:以最短回收停顿时间为目标,采用“标记-清除”算法,过程复杂,已逐步被G1替代。
  • 全堆收集器
    • G1:将堆划分为多个大小相等的独立区域,可以同时兼顾年轻代和老年代。它建立一个可预测的停顿时间模型,优先回收价值最大的区域。
    • ZGC / Shenandoah:新一代超低延迟(STW时间控制在10ms以内)的收集器,使用读屏障、染色指针等技术。

三、内存模型与GC机制的联系

  1. 管理范围:GC机制主要管理和回收的内存区域是堆和方法区(元空间)。程序计数器、虚拟机栈、本地方法栈这三个区域随线程生灭,内存自动释放,不涉及GC。
  2. 设计依据:JVM内存模型中的堆分区(年轻代/老年代)是分代收集理论的具体实现。这种划分是为了让GC算法能更高效地工作。
  3. 触发条件:GC的发生直接由内存模型各区域的容量状态触发(如Eden区满触发Minor GC,老年代满触发Full GC)。
  4. 分析基础:进行GC问题排查(如内存泄漏)时,必须结合内存模型分析对象在堆中的分配、引用和存活路径。

总结

特性JVM内存模型GC机制
核心关注点内存如何划分和组织,各个区域的作用和生命周期。内存如何自动回收和整理,确保可用空间。
主要交互区域堆、方法区(元空间)是GC的主战场。管理堆和方法区(元空间)。
关系GC机制是基于内存模型(特别是堆结构)而设计的。内存模型的划分(如分代)是为了让GC更高效。GC是维护内存模型(尤其是堆)健康运转的核心机制

简单来说:内存模型定义了“战场”的布局,而GC机制是在这个“战场”上进行清理和维护的“后勤部队”。理解两者是进行JVM性能监控和调优的基础。