Java的对象内存图
一、Java内存分配介绍
Java 虚拟机(JVM)在执行 Java 程序时会使用多个内存区域
- 栈:方法运行时所进入的内存,变量也是在这里
- 堆:new出来的东西会在这块内存中开辟空间并产生地址
- 方法区:字节码文件加载时进入的内存(class类、main方法等)
- 本地方法栈
- 寄存器
1. 堆区(Heap)
- 用途:
- 堆区是 Java 中用于动态分配内存的区域。所有的对象实例和数组都在堆中分配。
- 特点:
- 堆是共享的,所有线程都可以访问。
- 垃圾回收器负责自动管理堆内存,回收不再使用的对象。
- 生命周期:
- 对象在堆中创建时,只有在没有引用指向该对象时,才会被垃圾回收。
2. 栈区(Stack)
- 用途:
- 栈区用于存储局部变量和方法调用的上下文。
- 特点:
- 每个线程都有自己的栈,线程之间相互独立。
- 栈内存采用后进先出(LIFO)原则,方法调用时会将方法的局部变量和参数压入栈中,方法返回时会清除相应的栈帧。
- 生命周期:
- 局部变量的生命周期与方法调用的生命周期相同,方法结束后,局部变量会被自动释放。
3. 方法区(Method Area)
- 用途:
- 方法区用于存储类的结构信息,包括类的元数据、常量、静态变量和即时编译(JIT)编译后的代码等。
- 特点:
- 方法区在 JVM 中是共享的,所有线程都可以访问。
- 在 HotSpot JVM 中,方法区通常实现为永久代(PermGen)或元空间(Metaspace)。
- 生命周期:
- 类的元数据在类被加载时创建,类被卸载时释放。
4. 本地方法栈(Native Method Stack)
- 用途:
- 本地方法栈用于支持 Java 中调用的本地方法(Native Method)。本地方法是用其他语言(如 C 或 C++)编写的,并通过 Java Native Interface (JNI) 调用。
- 特点:
- 本地方法栈和 Java 栈类似,每个线程都有自己的本地方法栈。
- 存储本地方法调用的参数、局部变量和返回值。
- 生命周期:
- 本地方法栈的生命周期与线程相同,当线程结束时,相关的本地方法栈也会被销毁。
5. 寄存器(Registers)
- 用途:
- 寄存器是 CPU 中的高速存储器,用于存储临时数据和指令。JVM 在执行字节码时,常常会使用寄存器来提高执行效率。
- 特点:
- 寄存器的数量相对较少,访问速度非常快。
- JVM 在执行字节码时,将某些运算的结果直接存储在寄存器中,而不是在栈中进行操作。
- 使用:
- JVM 的具体实现会利用寄存器进行优化,尤其是在进行算术运算和逻辑运算时。
总结:
Java 虚拟机 在执行 Java 程序时,会在这些不同的内存区域中管理内存:
- 堆区:存储对象实例和数组,进行动态内存分配。
- 栈区:存储方法调用的局部变量和参数,支持方法的调用与返回。
- 方法区:存储类的结构信息和静态数据,支持反射和类的加载。
- 本地方法栈:用于支持调用本地方法,存储本地方法的局部变量和参数。
- 寄存器:用于存储临时数据和指令,提高执行效率。
二、一个对象的内存图
示例代码:
public class Student {
String name;
int age;
public void study() {
System.out.println("好好学习");
}
}
public class TestStudent {
public static void main(String[] args) {
Student s= new Student();
System.out.println(s);
System.out.println(s.name + "..." + s.age);
s.name = "阿明";
s.age = 21;
System.out.println(s.name + "..." + s.age);
s.study();
}
}
一个对象的内存图的虚拟机执行过程:
- 先在方法区加载
TestStudent
类的字节码文件,将main
方法进行临时存储 - 此时
main
方法会被加载到栈里 - 在方法区加载
Student
类文件,临时存储 Student s
——在栈区的main
方法中开辟以s
命名的内存空间new Student()
——在堆区开辟一个空间,并将Student
类里面的所有成员变量拷贝过去,并获得所有成员方法的地址- 默认初始化、显示初始化、构造方法初始化成员变量
- 将堆中的地址值赋值给栈区的局部变量
s
System.out.println(s)
——即打印s
的地址值- 后面的代码根据
s
的地址对在堆区的成员变量的值进行操作 s.study()
在调用堆区的方法地址,再找的方法区中进行操作,此时该方法会被加载进栈- 进行
study
方法里的System.out.println("好好学习")
操作 study
方法执行完毕,退出栈区,main
方法也执行完毕,退出栈区- 堆中的内存地址则没地调用,堆的内容也会被系统回收
三、二个对象的内存图的虚拟机执行过程:
和一个对象的内存图相比,方法区不必再次加载,堆区开辟新的空间即可,且两个空间互不影响
四、两个引用指向同一个对象:
示例代码:
public class Student {
String name;
int age;
public void study() {
System.out.println("好好学习");
}
}
public class TestStudent {
public static void main(String[] args) {
Student stu1= new Student();
stu1.name = "阿明";
Student stu2 = stu1;
stu2.name = "阿珍"
System.out.println(stu1.name + "..." + stu2.name);
stu1 = null;
System.out.println(stu1.name);
System.out.println(stu2.name);
stu2 = null;
}
}
不同在Student stu2 = stu1
相当于将stu1
保存的堆区new
出的地址赋值给stu2
,共同操作该堆区的空间,stu1 = null
——stu1
指向的方法消失,但还可以通过stu2
操作,但当stu2 = null
时堆区的空间不被调用,则会消失,堆区被清空,栈区的方法也操作完毕,出栈
五、基本数据类型和引用数据类型
在编程中,数据类型通常分为基本数据类型(或原始数据类型)和引用数据类型。
基本数据类型
-
定义:基本数据类型是语言内置的类型,通常直接表示简单的值。
-
值存储:基本数据类型的变量直接存储数据的值。
-
内存分配:在栈内存中分配,存储效率高。
-
示例:
- 整数(如
int
) - 浮点数(如
float
,double
) - 字符(如
char
) - 布尔值(如
boolean
)
- 整数(如
引用数据类型
-
定义:引用数据类型是用户定义的类型,包括对象和数组。
-
值存储:引用数据类型的变量存储的是对象的引用(地址),而不是对象本身的值。
-
内存分配:在堆内存中分配,通常占用更多内存。
-
示例:
- 类(如
String
,ArrayList
) - 接口
- 数组(如
int[]
)
- 类(如
主要区别总结
- 存储方式:基本数据类型存储值,引用数据类型存储地址。
- 内存管理:基本数据类型在栈中,引用数据类型在堆中。
- 性能:基本数据类型通常更高效,因为它们的内存占用较小。
六、this
的内存原理
this
的作用:
- 区分局部变量和成员变量
- 用于构造函数的重载
public class Example {
private int value;
public Example(int value) {
this.value = value; // 指向当前实例的变量
}
public Example() {
this(5); // 调用带参数的构造函数
}
}
解释:
- 当你创建一个
Example
对象时,可以使用new Example(10)
创建一个有参数的实例,this.value
将被赋值为 10。 - 如果使用
new Example()
,则会调用无参构造函数,它内部又调用了带参数的构造函数this(5)
,因此value
将被初始化为 5。
this
的本质:
所在方法调用者的地址值,管理调用者的数据
七、成员变量和局部变量的区别
1. 定义
- 成员变量:也称为实例变量,属于类的实例,定义在类的内部但在方法外部。
- 局部变量:定义在方法、构造函数或代码块内部,只在该特定作用域内有效。
2. 作用域
- 成员变量:在整个类中可访问,所有实例方法都可以访问,甚至可以通过对象实例访问。
- 局部变量:仅在定义它们的方法或代码块中有效,超出该作用域后无法访问。
3. 生命周期
- 成员变量:对象创建时分配内存,直到对象被垃圾回收时,成员变量的生命周期持续存在。
- 局部变量:在方法或代码块执行时分配内存,方法执行完毕后被销毁。
4. 默认值
- 成员变量:如果没有显式初始化,成员变量会被默认初始化为其类型的默认值(例如,整型为 0,布尔型为 false,引用类型为 null)。
- 局部变量:必须显式初始化,未初始化的局部变量无法使用,编译器会报错。
5.内存位置不同
- 成员变量:堆内存(对象里面)
- 局部变量:栈内存(方法里面)
6. 访问修饰符
- 成员变量:可以使用访问修饰符(如
public
,private
,protected
) 来控制访问权限。 - 局部变量:没有访问修饰符,仅在其所在的方法内有效。
示例:
public class Example {
// 成员变量
private int memberVariable = 10;
public void method() {
// 局部变量
int localVariable = 5;
System.out.println("Member Variable: " + memberVariable); // 可以访问
System.out.println("Local Variable: " + localVariable); // 可以访问
}
public void anotherMethod() {
// System.out.println(localVariable); // 编译错误:局部变量在此不可访问
}
}
此篇内容结束