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

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 中创建对象。

java 复制代码
public class Student {
    String name;      // 姓名
    double chinese;   // 语文成绩
    double math;      // 数学成绩

    public void printTotalScore() {
        double total = chinese + math;
        System.out.println(name + " 总分:" + total);
    }
}

public class StudentTest {
    public static void main(String[] args) {
        Student s = new Student();
        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 来回收。

相关推荐
夕除1 小时前
spring boot
java·spring boot·后端
想唱rap1 小时前
传输层协议之UDP
java·linux·网络·c++·网络协议·mysql·udp
河西石头1 小时前
听AI的血的教训!PPOCRLabel部署与PyQt5的安装避坑-百分百成功!
开发语言·人工智能·python·pyqt5安装·ppocrlabel的部署
野生技术架构师1 小时前
我总结了这份2026最新版Java面试题库(背完这一套就够了)
java·开发语言·面试
AIGC设计所1 小时前
网络安全8大就业领域和待遇对比!
运维·开发语言·网络·安全·web安全·php
xxjj998a1 小时前
PHP与汇编:从Web到硬件的编程差异
开发语言·汇编·php
可爱の小公举2 小时前
Java 后端程序员转 AI Agent 工程师:一条可执行学习路线
java·人工智能·学习
bestcxx2 小时前
多个维度对 Java、Python、C#、Go 这四种主流编程语言进行比较
java·python·c#
装杯让你飞起来啊2 小时前
Kotlin 条件判断 if / when 与智能转换 smart cast
开发语言·python·kotlin