Java内存入门讲解:从变量和对象开始
Java内存入门讲解:从变量和对象开始
一、先忘掉复杂概念,记住三个"地方"
想象你的程序运行时,有三个"地方"可以存放东西:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 栈内存 │ │ 堆内存 │ │ 方法区 │ │ (方法执行) │ │ (放对象) │ │ (放类信息) │ └─────────────┘ └─────────────┘ └─────────────┘ 简单理解: • 栈:像笔记本,记着正在做的事(方法执行) • 堆:像仓库,存放具体的东西(对象) • 方法区:像说明书,存放类是怎么定义的二、基础类型 vs 引用类型(最重要的区别)
2.1 基础类型变量:值直接放在变量里
intage=18;doubleprice=99.5;booleanisPass=true;内存中的样子:
栈内存 ┌─────────────────┐ │ age │ 18 │ ← 值直接存在变量里 ├─────────────────┤ │ price │ 99.5 │ ├─────────────────┤ │ isPass │ true │ └─────────────────┘2.2 引用类型变量:变量里存的是"地址"
Students1=newStudent();内存中的样子:
栈内存 堆内存 ┌──────────────┐ ┌─────────────┐ │ s1 │ 0x001 │────────→│ Student对象 │ └──────────────┘ │ name: null │ │ age: 0 │ └─────────────┘ 地址:0x001关键理解:s1这个变量里存的不是对象本身,而是对象在堆内存中的"门牌号"(0x001)
三、结合代码看内存变化
示例1:基础类型和引用类型的区别
publicclassMemoryDemo1{publicstaticvoidmain(String[]args){// 1. 基础类型变量inta=10;// 栈:a = 10// 2. 引用类型变量Studentstu=newStudent();// 栈:stu = 0x001// 堆:地址0x001存放Student对象stu.name="小明";// 堆中的name被赋值stu.age=18;// 堆中的age被赋值// 3. 赋值操作的区别intb=a;// 复制值:b = 10b=20;// a还是10,b变成20Studentstu2=stu;// 复制地址:stu2也指向0x001stu2.name="小红";// stu.name也变成"小红"(同一个对象!)System.out.println(stu.name);// 输出"小红"}}内存变化过程:
第1步:int a = 10; 栈:┌───┬────┐ │ a │ 10 │ └───┴────┘ 第2步:Student stu = new Student(); 栈:┌─────┬───────┐ 堆:┌─────────────┐ │ stu │ 0x001 │────────→│ name: null │ └─────┴───────┘ │ age: 0 │ └─────────────┘ 地址: 0x001 第3步:stu2 = stu; 栈:┌──────┬───────┐ 堆:┌─────────────┐ │ stu │ 0x001 │────┐ │ name: "小红" │ ├──────┼───────┤ └→ │ age: 18 │ │ stu2 │ 0x001 │───────→│ │ └──────┴───────┘ └─────────────┘示例2:方法调用时的内存变化
publicclassMemoryDemo2{publicstaticvoidmain(String[]args){intnum=5;// 步骤1changeNum(num);// 步骤2System.out.println(num);// 步骤5:输出5(没变!)Studentstu=newStudent();// 步骤3stu.name="小明";changeName(stu);// 步骤4System.out.println(stu.name);// 步骤6:输出"小红"(变了!)}staticvoidchangeNum(intn){n=100;// 这只是修改了栈中的n,不影响main里的num}staticvoidchangeName(Students){s.name="小红";// 通过地址找到堆中的对象,修改了真正的对象}}内存变化过程:
调用changeNum(num)时: main栈帧 changeNum栈帧 ┌─────────┐ ┌─────────┐ │ num: 5 │ │ n: 5 │ ← 复制了值 └─────────┘ └─────────┘ n = 100后: ┌─────────┐ ┌─────────┐ │ num: 5 │ │ n: 100 │ ← 只改了自己的 └─────────┘ └─────────┘ 方法结束:changeNum栈帧销毁,num还是5 调用changeName(stu)时: main栈帧 changeName栈帧 堆 ┌─────────┐ ┌─────────┐ ┌──────────┐ │ stu:0x001│─────→│ s:0x001 │───────→ │name:"小明"│ └─────────┘ └─────────┘ └──────────┘ s.name = "小红"后: ┌─────────┐ ┌─────────┐ ┌──────────┐ │ stu:0x001│─────→│ s:0x001 │───────→ │name:"小红"│ └─────────┘ └─────────┘ └──────────┘ 方法结束:changeName栈帧销毁,但堆中的对象已经被改了核心结论:
- 基础类型传递的是值(复制一份)
- 引用类型传递的是地址(指向同一个对象)
四、对象的创建过程(一步步看内存)
publicclassMemoryDemo3{publicstaticvoidmain(String[]args){// 第1步:声明变量Dogdog;// 栈中开辟空间,但还没指向任何对象// 第2步:创建对象dog=newDog();// 堆中创建Dog对象,把地址给dog// 第3步:给属性赋值dog.name="旺财";// 修改堆中的对象dog.age=3;// 第4步:调用方法dog.bark();// 通过地址找到对象,执行方法}}classDog{Stringname;// 引用类型属性,默认nullintage;// 基础类型属性,默认0voidbark(){System.out.println(name+"汪汪叫");}}完整内存图:
栈内存 堆内存 ┌─────────────────┐ ┌─────────────────────┐ │ main方法栈帧 │ │ Dog对象 (地址0x001) │ ├─────────────────┤ ├─────────────────────┤ │ dog │ 0x001 │───────→│ name │ 0x002 ──┐ │ └─────────────────┘ │ age │ 3 │ │ └─────────────────┘ │ │ ┌─────────────────────┘ ↓ 方法区(字符串常量池) ┌─────────────┐ │ "旺财" │ ← 字符串对象 └─────────────┘五、常见误区解释
误区1:new出来的东西都在堆里
// 正确理解Dogdog=newDog();// dog变量在栈(地址)// Dog对象在堆(具体数据)误区2:字符串是基础类型
// 错误理解Strings="hello";// 以为是基础类型?// 正确理解:String是引用类型Strings1="hello";Strings2="hello";// s1和s2指向同一个字符串对象(方法区的字符串常量池)误区3:方法结束所有内存都释放
publicvoidtest(){Dogdog=newDog();// dog变量在栈// 方法结束:栈中的dog变量消失// 但是:堆中的Dog对象还在!// 需要垃圾回收器来清理}六、一个完整示例:学生管理系统
publicclassStudentSystem{publicstaticvoidmain(String[]args){// 创建3个学生对象Students1=newStudent();s1.name="张三";s1.score=90;Students2=newStudent();s2.name="李四";s2.score=85;Students3=newStudent();s3.name="王五";s3.score=95;// 打印所有学生printStudent(s1);printStudent(s2);printStudent(s3);// 最高分的学生Studenttop=getTopStudent(s1,s2,s3);System.out.println("最高分:"+top.name);}staticvoidprintStudent(Students){// s指向堆中的某个Student对象System.out.println(s.name+":"+s.score);}staticStudentgetTopStudent(Studenta,Studentb,Studentc){Studentmax=a;// max也指向堆中的对象if(b.score>max.score)max=b;if(c.score>max.score)max=c;returnmax;// 返回的是地址}}classStudent{Stringname;intscore;}内存布局:
栈内存(main方法) 堆内存 ┌─────┬───────┐ ┌─────────────┐ │ s1 │ 0x001 │─────────────→│ name:"张三" │ ├─────┼───────┤ │ score:90 │ │ s2 │ 0x002 │─────┐ └─────────────┘ ├─────┼───────┤ │ ┌─────────────┐ │ s3 │ 0x003 │──┐ └───────→│ name:"李四" │ ├─────┼───────┤ │ │ score:85 │ │ top │ 0x003 │←─┘ └─────────────┘ └─────┴───────┘ ┌─────────────┐ │ name:"王五" │ │ score:95 │ └─────────────┘七、记忆口诀
基础类型存值,引用类型存址
- int、double、boolean:值直接放变量里
- 类、数组、接口:变量里是地址
栈管运行,堆放对象
- 栈:方法调用、局部变量
- 堆:new出来的东西
赋值等于复制
- 基础类型:复制值(独立)
- 引用类型:复制地址(共享)
方法参数传递
- 基础类型:传值的副本(改不了原变量)
- 引用类型:传地址的副本(能改对象内容)
八、练习题(检验理解)
// 说出下面代码的输出结果publicclassTest{publicstaticvoidmain(String[]args){intx=10;inty=x;y=20;System.out.println(x);// 输出?Personp1=newPerson();p1.age=30;Personp2=p1;p2.age=40;System.out.println(p1.age);// 输出?change(p1);System.out.println(p1.age);// 输出?}staticvoidchange(Personp){p.age=50;}}classPerson{intage;}// 答案:10, 40, 50掌握了这些,你就理解了Java内存最核心的部分。记住:变量要么存值,要么存地址,不会存对象本身!
