深入理解JAVA内存模型(JMM)

Java内存模型(JMM),也被称为运行时数据区

前言:

        在 Java 开发中,理解 Java 内存模型(Java Memory Model),简称JMM,是掌握 JVM 工作原理、排查内存溢出问题、优化程序性能的关键。JMM 本质上定义了 Java 程序运行时数据的存储结构与访问规则,明确了哪些数据在线程间共享、哪些数据为线程私有。本文将从 “线程共享区”“线程私有区”“字符串常量池” 三大核心模块入手,带你全面拆解 Java 内存模型的底层逻辑。

一.运行时数据区域划分

        Java程序运行时,JVM会将内存划分为线程共享区线程私有区两大块,此外还有特殊的 “字符串常量池”(其存储位置在 JDK 1.8 后有调整)。

JDK1.8之分为:线程共享(Heap堆区、MethodArea方法区)、线程私有(虚拟机栈、本地方法栈、程序计数器)
JDK1.8以分为:线程共享(Heap堆区、MetaSpace元空间)、线程私有(虚拟机栈、本地方法栈、程序计数器)

在1.8之后,方法区被元空间替代,并保存在直接内存中。由于在实际开发中广泛使用JDK1.8,本次也是围绕JDK1.8的版本进行解释说明。

二.线程共享区

1.堆

        堆是 Java 内存中最大的一块区域,主要存放对象实例和数组,同时也是 GC(垃圾回收)最频繁的区域。为了提高 GC 效率,堆采用 “分代管理” 思想,分为 “新生代” 和 “老年代” 两大块,比例默认是 1:2(新生代占 1/3,老年代占 2/3)。

新生代:

        存放刚创建的对象,特点是 “对象生命周期短、垃圾产生频繁”,采用 “复制算法” 进行 GC。新生代又分为3个区域。

        Eden 区:新对象优先在 Eden 区分配空间(占新生代的 80%)

        两个 Survivor 区(S0、S1):比例均为 10%,用于存储 YGC 后未被回收的对象(类似 “临时缓存区”,两个区交替使用)。

老年代:

        存放 “经历 15 次 YGC 仍未被回收的对象” 或 “大对象”(如超大数组、大字符串),采用 “标记 - 清除 / 标记 - 整理算法” 进行 GC,GC 频率较低但耗时较长。

创建对象的内存分配流程:

        创建一个新对象,在堆中的分配内存。
        大部分情况下,对象会在Eden区生成,当Eden区装填满的时候,会触发 Young Garbage Collection,即 YGC 垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。
        依然存活的对象会被移送到Survivor区。Survivor区分为s0和s1两块内存区域。每次YGC的时候,它们将存活的对象复制到未使用的Survivor空间(s0或s1),然后将当前正在使用的空间完全清除,交换两块空间的使用状态。每次交换时,对象的年龄会加+1。
        如果YGc要移送的对象大于Survivor区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,在JVM中一个对象从新生代晋升到老年代的阀值默认值是15,可以在Survivor区交换14次之后,晋升至老年代。流程图如下:

注意:堆区最容易出现的就是OutofMemoryError错误,这种错误的表现形式会有以下两种:
1.OutOfMemoryError:GC Overhead Limit Exceeded:当JVM花太多时间执行垃圾回收,并且只能回收很少的堆空间时,就会发生此错误。
2.OutOfMemoryError:Java heap space:假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误。

2.元空间

        主要用于存放类信息、常量、静态变量,JIT即时编译器编译后的机器代码等数据。这里我们顺便说一下“方法区”和“永久代”。

        JDK1.6 :HotSpot JVM使用 Method Area 方法区存储,也叫永久代(Permanent Generation)。
        方法区和“永久代”的区别:

        1.方法区是JVM的规范,而永久代是JVM规范的一种实现,并且只有HotSpotJVM才有永久代,而对于其他类型的虚拟机,如JRockit(Oracle)、J9(IBM)并没有;
        2.方法区是一片连续的堆空间,当JVM加载的类信息容量超过了最大可分配空间,虚拟机会抛出OutofMemoryError:PermGenspace的Error。
        3.永久代的Gc是和老年代(oldgeneration)捆绑在一起的,无论谁满了,都会触发永久代和老年代的垃圾收集。

JDK1.8:正式移出永久代,被Meta Space原空间代替。

        由于PermGen内存经常会溢出,容易抛出java.lang.OutofMemoryError:PermGen错误,因此移除了永久代,减少了内存溢出风险。元空间的本质和永久代类似,都是对JVM规范中方法区的一种具体实现。不过元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过运行参数来指定元空间的大小。

三.线程私有区

1.程序计数器

        程序计数器是 Java 内存模型中唯一不会抛出 OutOfMemoryError的区域,它随着线程的创建而创建,随着线程的结束而死亡。体积极小,作用是记录当前线程执行的 “字节码行号”。

        核心功能

        1.多线程切换时,保存当前线程的执行位置:当线程被 CPU 切换出去后,再次恢复时能通过程序计数器找到上次执行的字节码指令,保证程序流程不中断;

        2.字节码解释器的 “导航仪”:解释器通过修改程序计数器的值,依次读取字节码指令,实现循环、分支(if-else)、跳转等代码逻辑。

2.虚拟机栈

        Java 虚拟机栈(简称 “栈”)采用先进后出(FILO) 的栈结构,用于管理方法的调用和执行过程。每个方法被调用时,JVM 会在栈中创建一个 “栈帧”,栈帧是方法执行的 “最小单位”。

1.栈帧的组成结构:

  • 局部变量表:存储方法的局部变量(如基本数据类型、对象引用),容量在编译期确定;

  • 操作数栈:方法执行过程中用于临时存放操作数(如算术运算、方法参数传递);

  • 动态链接:将方法中的符号引用(如类名、方法名)转换为实际内存地址;

  • 方法返回地址:记录方法执行完成后,需要返回的 “调用者位置”(如回到调用该方法的下一行指令)。

2.栈的运行原理(以方法调用为例)

假设存在方法调用链:方法1 → 方法2 → 方法3→ 方法4,栈的变化过程如下:

  1. 方法1被调用时,创建方法1栈帧并压入栈顶,成为 “当前活动栈帧”;

  2. 方法1调用方法2,创建方法2栈帧压入栈顶,替代方法1成为活动栈帧;

  3. 方法2调用方法3,创建方法3栈帧压入栈顶;替代方法2成为活动栈帧;

  4. 方法3调用方法4,创建方法4栈帧压入栈顶;替代方法3成为活动栈帧;

  5. 方法4执行完成,弹出方法4栈帧,方法3恢复为活动栈帧;

  6. 方法3执行完成,弹出方法3栈帧,方法2恢复为活动栈帧;

  7. 方法2执行完成,弹出方法2栈帧,方法1恢复为活动栈帧;

  8. 方法1执行完成,弹出方法1栈帧,栈为空。

        在活动线程中,只有位于栈顶的帧才是有效的,称为当前活动栈帧,代表正在执行的当前方法。在JVM执行引擎运行时,所有指令都只能针对当前栈帧进行操作。虚拟机栈通过pop和push的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。流程图如下:

3.虚拟机栈常见的异常

  • StackOverFlowError:当线程请求的栈深度超过虚拟机栈的最大深度(如递归调用无终止条件)时抛出;

  • OutOfMemoryError:若虚拟机栈支持动态扩展(大部分 JVM 默认支持),当扩展时无法申请到足够内存时抛出。

3.本地方法栈

        本地方法栈与 Java 虚拟机栈的结构和功能类似,唯一区别是:虚拟机栈管理 Java 方法的执行,本地方法栈管理 Native 关键字修饰的本地方法的执行。也会出现StackOverFlowError和OutOfMemoryError异常。

三.特殊区域:字符串常量池

        字符串常量池(String Constant Pool)是 Java 中专门用于存储字符串常量的 “缓存区域”,目的是减少字符串对象的重复创建,提高内存利用率。其存储位置在内存(Heap)中。

1.字符串的创建方式

        直接赋值:String s="abc";  执行逻辑是先检查字符串常量池中是否存在 “abc”,若存在,直接返回常量池中的 “abc” 对象引用;不存在,在常量池中创建 “abc” 对象,再返回引用。

        new关键字创建:String s=new String("abc"); 执行逻辑是先检查字符串常量池中是否存在 “abc”,若存在,直接将 s 指向 “abc” ;不存在则创建 “abc” 对象,然后将 s 指向字符串常量池中的对象并返回引用。

2.String.intern() 方法

        intern()方法是 String 类的 native 方法,作用是 “强制将字符串对象加入常量池”,调用str.intern()时,先检查常量池中是否存在与 str内容相同的字符串,若存在,返回常量池中的字符串引用;若不存在,将 str的内容加入常量池,再返回常量池引用。

四.总结

        线程共享区是 GC 的核心区域,线程私有区与线程生命周期绑定,字符串常量池是优化字符串存储的关键。掌握 Java 内存模型,不仅能帮助我们写出更高效、更稳定的代码,也是应对 JVM 相关面试的必备基础。希望本文能为你搭建清晰的 JMM 知识框架,后续可结合 “垃圾回收算法”“内存溢出排查工具” 等内容进一步深入习!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值