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.class 和 Student.class,通过类加载器将它们的类信息 存入方法区。方法区中记录了:

  • 类的访问修饰符、父类、接口
  • 字段信息(name、chinese、math 的名称、类型、偏移量)
  • 方法信息(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 字节)、chinese 和 math(各 8 字节),总共约 32~40 字节。
  3. 零值初始化 :将分配的内存空间全部初始化为默认值。于是:
    • name → null
    • chinese → 0.0
    • math → 0.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 来回收。

相关推荐
用户13051807121534 分钟前
JDBC学习DAY2:从Statement到PreparedStatement
java
蛋蛋的就会顺顺的1 小时前
hot100——矩阵
java·数据结构·算法·leetcode·力扣
周杰偷奶茶1 小时前
【Java】数组的定义和使用(附管理系统实战案例)
java·开发语言
橙子圆1232 小时前
JUC之集合类不安全
java·开发语言
阿里嘎多学长2 小时前
2026-09-29 GitHub 热点项目精选
开发语言·程序员·github·代码托管
Bruce_Liuxiaowei2 小时前
网页版 DeepSeek “harness desktop“ WebGL 上下文创建失败排查实录
开发语言·javascript·webgl
狗凯之家源码网2 小时前
PHP 用户反馈系统源码深度评测与实战指南
开发语言·php·用户反馈系统
小溪学编程2 小时前
从 C++ 的规范变迁看语言发展
jvm·c++·面试
谢亮_vipxieliang3 小时前
Stream API 不是银弹:常见坑与性能建议
java·开发语言
迅猛龙办公室3 小时前
实现第一个python程序(HelloWorld)
开发语言·python