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 来回收。

相关推荐
小灰灰搞电子1 分钟前
Rust+Slint 实现FTP客户端调试助手源码分享
开发语言·后端·rust
外收内放3 分钟前
面向对象高级(实战案例)
开发语言·python·学习
Javatutouhouduan4 分钟前
阿里1658页Java面试题总结(含答案)
java·高并发·java面试·java面试题·后端开发·java程序员·java八股文
秋天的怀念_David21 分钟前
算法一千零一夜 · 第十三夜 幻方,武侠世界里的数学迷宫
java·程序人生·算法
专业程序开发源41 分钟前
springboot欧缇蔓月子中心管理系统82697-计算机课程设计、毕业设计
java·spring boot·后端·python·spring·django·课程设计
秋天的怀念_David1 小时前
算法一千零一夜 · 第十四夜 幻方与奇妙的二进制
java·程序人生·算法
thefool1122661 小时前
网络编程^
java
fanruitian1 小时前
python else用法
开发语言·python
FRing031 小时前
JavaScript入门到精通二十六(变量提升、函数剩余参数、展开运算符、ES6箭头函数)
开发语言·前端·javascript·es6·箭头函数
云深麋鹿1 小时前
C++ | 基于红黑树实现mymap与myset
开发语言·c++