☕ Java 内存与对象原理详解

📅 2026/7/18 16:44:01
☕ Java 内存与对象原理详解
一句话总结Java 中一切对象都在堆上分配变量只是引用指针理解这一点就理解了 Java 内存模型的一半。一、JVM 内存分区速览┌─────────────────────────────────────────────────────────┐ │ JVM 内存结构 │ ├──────────────┬──────────────┬──────────────┬─────────────┤ │ 堆 Heap │ 栈 Stack │ 方法区 │ 本地方法栈 │ │ (线程共享) │ (线程私有) │ (线程共享) │ (线程私有) │ ├──────────────┼──────────────┼──────────────┼─────────────┤ │ · 所有对象 │ · 局部变量 │ · 类元信息 │ · native │ │ · 所有数组 │ · 方法调用栈帧 │ · 静态变量 │ 方法调用 │ │ · 字符串常量池 │ · 操作数栈 │ · 运行时常量池 │ │ │ (引用在栈) │ · 返回地址 │ · JIT编译代码 │ │ └──────────────┴──────────────┴──────────────┴─────────────┘区域存储内容特点堆 Heapnew出来的对象、数组GC 管理最大的一块栈 Stack基本类型值、对象引用、方法帧每个线程独立方法结束自动释放方法区Class 元数据、static 变量、常量池JDK8 用 Metaspace 实现二、对象的内存分配全过程PersonpnewPerson(张三,25);这一步背后发生了什么栈 (Stack) 堆 (Heap) ┌──────────────┐ ┌─────────────────────┐ │ │ │ Person 对象 │ │ p ──────────┼────────────────│ ┌─────────────────┐│ │ (引用) │ │ │ name 张三 ││ │ 0x1000 │ │ │ age 25 ││ │ │ │ │ [Class 指针] ││ └──────────────┘ │ │ [Mark Word] ││ │ └─────────────────┘│ └─────────────────────┘分步拆解① Person p → 栈上声明一个引用变量目前 null ② new Person() → 堆上分配内存初始化对象 ├─ Mark Word (8B / 12B) — 锁状态、GC 标记、hashCode ├─ Klass 指针 (4B / 8B) — 指向方法区的 Class 对象 └─ 实例数据 — name 引用 age int ③ → 把堆地址赋给栈上的引用变量 p指针压缩64 位 JVM 默认开启-XX:UseCompressedOops堆 32GB 时引用占 4 字节而非 8 字节。对象内存布局微观┌──────────────┐ ← 低地址 │ Mark Word │ 对象头hashCode、GC 年龄、锁标志、偏向线程 ├──────────────┤ │ Klass ptr │ 对象头指向方法区 Class? 元数据 ├──────────────┤ │ field_1 │ │ field_2 │ 实例数据按字段类型排列long/double 优先对齐 │ ... │ ├──────────────┤ │ padding │ 对齐填充补齐到 8 字节的倍数 └──────────────┘ ← 高地址三、对象传给方法 —— 传的是什么Java 只有值传递。当值是引用时传递的是引用的副本。voidchange(Personperson){person.setName(李四);// 修改的是堆上的同一对象personnewPerson(王五);// 只改了副本指向不影响调用方}PersonpnewPerson(张三);change(p);// p.getName() → 李四 (对象被改了)// p 还是指向原来的地址 (引用副本的重新赋值不影响原引用)内存示意图调用 change(p) 时 调用方栈帧 被调用方栈帧 ┌──────────┐ ┌──────────┐ │ p ───────┼──┐ ┌──┤ person ──┼──┐ person 是 p 的副本 └──────────┘ │ │ └──────────┘ │ │ ┌──────────┐│ │ └───│ Person │───────────────┘ │ name张三│ 两个引用指向同一对象 └──────────┘ 执行 person new Person(王五) 后 调用方栈帧 被调用方栈帧 ┌──────────┐ ┌──────────┐ │ p ───────┼──┐ ┌──┤ person ──┼────────────┐ └──────────┘ │ │ └──────────┘ │ │ ┌──────────┐│ ┌──────────┐ │ └───│ Person ││ │ Person │──┘ │ name张三│ │ name王五│ └──────────┘ └──────────┘ p 仍然指向原对象 person 指向了新对象核心规则方法可以改变参数引用所指对象的内容但不能改变调用方引用本身的值。四、多个变量指向同一个对象别名问题PersonanewPerson(小明);Personba;// b 和 a 指向同一个对象栈 堆 ┌──────────┐ │ a ───────┼──────────────┐ ├──────────┤ │ ┌─────────────────┐ │ b ───────┼──────────────┘ │ Person(小明) │ └──────────┘ ┌─────│ name 小明 │ │ └─────────────────┘ │ ┌──────────┐ │ │ c ───────┼──────────────┘ 一个对象三个引用 └──────────┘陷阱你以为在复制实际在共享int[]arr1{1,2,3};int[]arr2arr1;// 不是复制数组是复制引用arr2[0]999;// arr1[0] 也变成 999System.out.println(arr1[0]);// 输出 999正确做法深拷贝// 浅拷贝基本类型数组有效int[]arr2arr1.clone();// 深拷贝对象数组需要遍历Person[]group2newPerson[group1.length];for(inti0;igroup1.length;i){group2[i]newPerson(group1[i].getName());// 每个都 new}五、this关键字在内存中的秘密this不在对象里而是方法的隐式参数。编译器的魔法classPerson{privateStringname;publicvoidsetName(Stringname){this.namename;// this 区分参数和字段}}编译器实际把它变成这样// 编译器转换后的等价形式publicstaticvoidsetName(Personthis,Stringname){// this 作为第一个参数this.namename;}调用的真相p.setName(张三);// 编译器实际执行// Person.setName(p, 张三); ← p 作为 this 传进去内存视角调用 p.setName(张三) 时的栈帧 ┌─────────────────────────┐ │ setName() 栈帧 │ │ │ │ this ──────────────┐ │ this 指向调用该方法的对象 │ name 张三 │ │ │ │ │ 堆 │ [局部变量表] │ │ ┌─────────────┐ │ [0] this (p) │──┼─│ Person 对象 │ │ [1] name 参数 │ │ │ name ? │ └─────────────────────────┘ └─────────────┘this的四种用法用法示例说明区分字段this.name name当参数和字段同名时调用构造器this(默认值)必须在构造器第一行返回自身return this链式调用 (Builder 模式)传参method(this)把当前对象传给别的方法// Builder 模式中的 thispublicPersonBuildersetName(Stringname){this.namename;returnthis;// ← 返回当前对象支持链式调用}// personBuilder.setName(A).setAge(20).build();六、补充知识点6.1 基本类型 vs 引用类型基本类型 (8种) 在栈上直接存值 ┌──────────┐ │ int x42 │ → 栈帧里直接存 42 │ double │ │ boolean │ │ ... │ └──────────┘ 引用类型在栈上存地址堆上存数据 ┌──────────┐ ┌──────────────┐ │ String s ├──────│ hello │ │ int[] arr├──────│ [I0x... │ │ Object o ├──────│ Object0x... │ └──────────┘ └──────────────┘6.2 字符串的特殊待遇Strings1hello;// 字面量 → 字符串常量池 (堆内)Strings2hello;// 复用常量池s1 s2Strings3newString(hello);// 强制 new → 堆上新对象s1 ! s3Strings4s3.intern();// 手动入池 → s4 s1字符串常量池 (JDK7 在堆中) 普通堆对象区 ┌────────────────────┐ ┌────────────────┐ │ hello ◄── s1 │ │ new String │ │ ◄── s2 │ │ (hello) ◄─ s3│ │ ◄── s4 │ │ value ────────┼──┐ └────────────────────┘ │ hash 0 │ │ └────────────────┘ │ 指向同一个 char[] ◄─┘6.3 和 equals() 的本质区别操作比较的是说明栈上的值基本类型比数值引用类型比地址equals()堆上的内容默认也是需要重写才能比内容Personp1newPerson(张三);Personp2newPerson(张三);p1p2// false — 地址不同p1.equals(p2)// 取决于是否重写了 equals()Stringstr1abc;Stringstr2abc;str1str2// true — 常量池复用6.4 null 的本质null 就是一个特殊的空地址 (0x0000)表示不指向任何对象 ┌──────────┐ │ p ───────┼─── null (0x0000) └──────────┘ p.getName() → NullPointerException // JVM 尝试通过 null 地址访问对象找不到 → 报错6.5 垃圾回收 (GC) 的判定可达性分析GC Roots (栈引用、静态变量、JNI引用...) │ ├──→ obj_A ──→ obj_B ──→ obj_C 可达存活 │ └──→ obj_D ──→ obj_E 可达存活 obj_F ──→ obj_G 不可达将被回收 (没有任何 GC Root 能到达)PersonpnewPerson();// p 是 GC Root指向的对象存活pnull;// 引用断裂 → 对象不可达 → 等待 GC6.6 内存泄漏示例// ⚠️ 经典泄漏集合持有引用却不清理Listbyte[]cachenewArrayList();while(true){cache.add(newbyte[1024*1024]);// 1MB永远不释放}// → OutOfMemoryError: Java heap space// ✅ 解决用 WeakHashMap / 定期清理 / 软引用七、一张图总结全部┌─────────────────────────────────────────────────────────────┐ │ JVM 运行时数据区 │ │ │ │ ┌── 栈 (Stack) ───────────────────────────────────────┐ │ │ │ 每个线程独占 │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ main() 栈帧 │ │ method() 帧 │ │ method2()帧 │ │ │ │ │ │ int a 1 │ │ this ──────┼──│──│───────────┼─┐ │ │ │ │ │ Person p────┼──│──│───────────┼──│──│───────────┼─┤ │ │ │ │ └─────────────┘ │ local vars │ │ local vars │ │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ │ ┌── 堆 (Heap) ─────────────────────────────────────┐ │ │ │ │ 所有线程共享 │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ │ Person1000 │ │ int[]2000 │ │ │ │ │ │ │ name张三 │ │ [1,2,3] │◄──────────────┼───┘ │ │ │ │ age25 │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ ┌──────────────────────────────┐ │ │ │ │ │ 字符串常量池 │ │ │ │ │ │ hello → 张三 → abc │ │ │ │ │ └──────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ ┌── 方法区 / Metaspace ──────────────────────────────┐ │ │ │ · Person.class 元信息 │ │ │ │ · static 变量 (静态字段) │ │ │ │ · 方法字节码 │ │ │ │ · 运行时常量池 │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘核心法则记住这些就够了#法则反例1对象new在堆引用在栈—2Java 只有值传递引用传递的是地址的副本方法里新对象不影响外部3比地址equals()比内容需重写new String(a) new String(a)→ false4this是编译器偷偷塞给你的隐式参数静态方法里不能用 this5对象不再被任何 GC Root 引用 → 等待回收p null让原对象失联6多个变量指向同一个对象 → 改一个全都可见想独立请深拷贝延伸阅读JVM 规范 §2.5运行时数据区、《深入理解 Java 虚拟机》周志明、JOL (Java Object Layout) 工具可打印对象内存布局。