Java对象在计算机中的执行原理:从JVM内存模型到对象创建全过程
2026/5/5 14:47:27 网站建设 项目流程

Java对象在计算机中的执行原理:从JVM内存模型到对象创建全过程

你知道new Student()背后的内存操作吗?为什么对象的属性会有默认值?引用变量和实际对象存储在哪里?本文将带你深入 JVM 内存模型,图解对象创建的全流程,让你彻底理解 Java 对象的内存执行原理。

一、引言

作为一名 Java 开发者,我们每天都在new对象,却很少有人真正思考:对象到底被放在了哪里?引用又是如何指向它的?理解对象的执行原理,不仅能帮助我们写出更高效的代码,还能快速定位内存泄漏、空指针等疑难问题。

本文基于 JVM 规范,结合一个简单的Student类,详细剖析从.java源代码到运行时内存分配的完整过程。

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

Java 虚拟机在运行程序时,会将内存划分为几个不同的区域。每个区域都有特定的职责和生命周期。

区域存储内容线程共享主要特点
程序计数器当前线程执行的字节码行号线程私有很小,不会 OOM
Java 虚拟机栈局部变量、操作数栈、方法出口等线程私有每个方法执行时创建栈帧
本地方法栈native方法服务线程私有与虚拟机栈类似
所有new出来的对象、数组线程共享GC 主要管理区域,可设置大小
方法区类信息、常量、静态变量、JIT 编译后的代码线程共享逻辑上属于堆,常被称为“永久代”或“元空间”

下图展示了这五个区域的逻辑关系(箭头表示引用,不是继承):

线程私有

线程共享

栈中的引用指向

类信息定义对象结构


存储对象实例

方法区
存储类结构、常量、静态变量

程序计数器

Java虚拟机栈
栈帧1 栈帧2 ...

本地方法栈

三、一个完整的示例:Student 类

我们定义一个简单的Student类,包含两个成员变量和一个方法,然后在main中创建对象。

publicclassStudent{Stringname;// 姓名doublechinese;// 语文成绩doublemath;// 数学成绩publicvoidprintTotalScore(){doubletotal=chinese+math;System.out.println(name+" 总分:"+total);}}publicclassStudentTest{publicstaticvoidmain(String[]args){Students=newStudent();s.name="小张";s.chinese=89;s.math=90;s.printTotalScore();}}

四、对象在内存中的执行过程(逐步图解)

步骤 1:类加载 —— 将.class文件读入方法区

JVM 首先找到StudentTest.classStudent.class,通过类加载器将它们的类信息存入方法区。方法区中记录了:

  • 类的访问修饰符、父类、接口
  • 字段信息(namechinesemath的名称、类型、偏移量)
  • 方法信息(printTotalScore的字节码指令)
  • 常量池(例如字符串"小张"" 总分:"等)

注意:此时还没有任何Student对象,只有类的蓝图。

步骤 2:main 方法执行 —— 创建栈帧

JVM 为main线程分配一个虚拟机栈,然后为main方法创建一个栈帧(Stack Frame)。栈帧中包含:

  • 局部变量表(存放args、局部变量s
  • 操作数栈(用于计算)
  • 方法出口信息

此时局部变量s是一个引用类型,尚未赋值(默认为null)。

步骤 3:执行new Student()—— 分配堆内存

当执行到new Student()时,JVM 会做以下几件事:

  1. 检查类是否已加载:如果方法区没有Student类信息,先进行类加载。
  2. 分配内存:在堆中为对象分配一块连续的内存空间。对象所需的内存大小在类加载时就已经确定(实例变量 + 对象头)。本例中,对象头约 12~16 字节(取决于 JVM),加上name引用(4 或 8 字节)、chinesemath(各 8 字节),总共约 32~40 字节。
  3. 零值初始化:将分配的内存空间全部初始化为默认值。于是:
    • namenull
    • chinese0.0
    • math0.0
  4. 设置对象头:对象头中存储了哈希码、GC 分代年龄、锁状态标志,以及指向方法区中Student类元数据的指针。
  5. 执行构造方法:如果定义了构造器,会执行<init>方法。本例中没有显式构造器,但会执行默认构造器,该构造器不做额外赋值。

最终,堆中有了一个完整的Student对象,假设其起始地址为0x4f3f5b4f

步骤 4:将引用赋值给栈中的变量

new操作返回该对象的引用(即地址0x4f3f5b4f),然后赋值给栈帧中的局部变量s。此时,s就是一个指向堆中Student对象的引用(存放在栈中)。

方法区

引用

类指针

局部变量 s
0x4f3f5b4f

Student 对象
地址: 0x4f3f5b4f
name = null
chinese = 0.0
math = 0.0
对象头...

Student.class
类元数据
字段偏移量
方法字节码

步骤 5:属性赋值 —— 修改堆中对象的状态

执行s.name = "小张";等语句时,JVM 通过s中的地址找到堆中对象,然后修改对应字段的值。注意"小张"是一个字符串字面量,它被存放在方法区的运行时常量池中(JDK 1.7 之后字符串常量池在堆中,为简化本文按经典模型理解)。对象中的name引用指向常量池中的"小张"

步骤 6:调用方法 —— 创建新的栈帧

执行s.printTotalScore();时,JVM 会为printTotalScore方法创建一个新栈帧压入当前线程的栈中。该方法的局部变量表包含this(隐式参数,指向当前对象)和局部变量total。方法执行完毕后,栈帧被弹出,程序返回main继续执行。

五、完整流程图 —— 对象从生到灭

下面用 Mermaid 流程图总结new Student()的完整执行路径:

“执行 StudentTest.main”

“类加载器加载 StudentTest 和 Student 到方法区”

“虚拟机栈为 main 方法创建栈帧”

“执行 new Student”

“类 Student 已加载?”

“加载 Student 类信息到方法区”

“堆中分配内存”

“零值初始化: name=null, chinese=0.0, math=0.0”

“设置对象头”

“执行默认构造方法”

“返回对象地址 0x4f3f5b4f”

“将地址赋值给栈中的局部变量 s”

“s.name = "小张" 等,修改堆中数据”

“调用 printTotalScore,创建新栈帧”

“执行方法字节码”

“方法结束,弹出栈帧”

“main 继续或结束”

六、补充:String 与 StringBuffer 在内存中的区别

在你的原始内容中提到了String s1;StringBuffer sb;。这里补充一下它们在内存上的差异:

  • String:不可变类,任何修改都会创建新的String对象。字面量"abc"存储在常量池,new String("abc")会在堆中创建对象(即使常量池中已有)。
  • StringBuffer:可变类,内部维护一个字符数组,修改操作(append)直接在原对象上进行,不会创建新对象。因此频繁的字符串拼接推荐使用StringBuffer(或StringBuilder)。

从内存角度看,StringBuffer对象的堆内存中会有一个char[]数组的引用,该数组也在堆中。而String对象则持有一个char[]引用,且该数组不可变。

七、常见误区与注意事项

  1. “对象在栈中”?错。对象一定在堆中,栈中只存放引用类型的变量(即地址)。
  2. 基本类型变量一定在栈中?不一定。局部变量中的基本类型在栈中,但类的成员变量(实例变量)在堆中,静态变量在方法区。
  3. 方法区也会 OOM?是的,如果动态加载大量类或字符串常量过多,可能导致元空间溢出。
  4. 引用传递的本质:Java 只有值传递,当参数是引用类型时,传递的是引用的副本(地址值),所以可以通过引用修改堆中的对象内容,但无法让原引用指向一个新对象。

八、总结

  • JVM 内存模型分为线程共享(堆、方法区)和线程私有(栈、程序计数器、本地方法栈),各自负责不同的数据存储。
  • 对象创建过程:类加载 → 堆中分配内存 → 零值初始化 → 设置对象头 → 执行构造方法
  • 栈中的引用变量指向堆中的对象,方法区则保存类的元数据。
  • 理解这一执行原理,有助于写出更健壮、高效的代码,也能更轻松地排查内存相关 Bug。

如果你曾经对NullPointerException感到困惑,或者疑惑为什么s = null后对象还没消失,现在你应该有了答案:栈中的引用消失了,但堆中的对象要等待 GC 来回收。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询