完成本篇文章参考另外两篇大牛的文章:
https://blog.csdn.net/bruce128/article/details/79357870
https://blog.csdn.net/qq_36743482/article/details/78527312
内存区域概览
明确期间
首先要明确的是我们讨论的各类型变量占用内存是指在运行期间所占系统内存,也就是你开启这个java程序后所占的内存。这点需要明确,因为你没有运行项目时,他就是一个java文件,静态的,他跟word文档一样占用几kb的字符内存这个就跟你代码长短有关系跟他里面写的什么是没关系的。
所以我觉得有些文章讨论的是在编译期占内存,我觉得是没有理解内存的。因为编译期间,不过是把.java
文件编译成.class
文件,这还是一个静态的文件,项目还没有启动。只有等到使用JVM加载.class
文件这个时候程序才启动,才有不同变量占用内存的说法。
Java程序运行在JVM(Java Virtual Machine,Java虚拟机)上,可以把JVM理解成Java程序和操作系统之间的桥梁,JVM实现了Java的平台无关性。
所以在学习Java内存分配原理的时候一定要牢记一切都是在JVM中进行的,JVM是内存分配原理的基础与前提。
内存区域
- 栈:保存局部变量的值(基本数据类型的值,对象的引用,和方法内的形参变量)。栈中的所有变量会在结束生命周期自动被清理。
- 堆:用来存放动态产生的数据,比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。
- 方法区(method)(元空间实现,逻辑上在堆中),用于存放:
- 虚拟机加载的类信息(构造方法和接口定义)(元数据区)
- 静态变量和方法;(在jdk1.8后放入堆中)
- 常量池(常量池:JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用(1)。池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。 在JDK1.8中把字符串常量从常量池移除,放到了堆中。 这也就是有道面试题:
String s = new String(“xyz”);
产生几个对象?答:一个或两个,如果常量池中原来没有”xyz”,就是两个。如果常量池中没有则先在常量池中创建,然后再在堆中创建。常量池为堆中的一块区域。
这里介绍的是JDK1.8 JVM运行时内存数据区域划分。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存,逻辑上可以认为在堆中。
案例说明
实例代码1
class BirthDate {
//成员变量
private int day;
private int month;
private int year;
public BirthDate(int d, int m, int y) {//d,m,y均为形参局部变量
day = d;
month = m;
year = y;
}
}
public class Test {
public static void main(String args[]) {
int date = 9;//局部变量
Test test = new Test();//创建对象
test.change(date);
BirthDate d1 = new BirthDate(7, 7, 1970);//创建对象
}
public void change1(int i) {
i = 1234;//局部变量
}
下面分析程序执行时候的内存分配情况:
- main方法开始执行:
int date = 9;
date局部变量,存放在stack。 Test test = new Test();
test为对象引用,存在stack中,对象(new Test())存在heap中。test.change(date);
i为形参局部变量,存栈中。当方法change执行完成后,i结束生命自动在栈中被清理掉。BirthDate d1= new BirthDate(7,7,1970);
d1 为对象引用,存在栈中,对象(new BirthDate())存在堆中,day,month,year为成员变量,它们存储在堆中(new BirthDate()
里面。其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中。 当BirthDate构造方法执行完之后,d,m,y将从栈中消失。- main方法执行完之后,所有存在于main方法的局部变量date,test,d1引用将从栈中消失,而在堆中的对象
new Test(),new BirthDate()
将等待垃圾回收。
实例代码2
Student类
public class Student {
int score;
int age;
String name;
Computer computer;
public void study() {
System.out.println("studying...");
}
}
Computer类
public class Computer {
int price;
String brand;
}
测试类:已标出步骤
public class Test {
public static void main(String[] args) {
//1-5
Student stu = new Student();
//6
stu.name = "xiaoming";
//7
stu.age = 10;
//8
stu.study();
Computer c = new Computer();
c.brand = "Hasse";
System.out.println(c.brand);
//9
stu.computer = c;
System.out.println(stu.computer.brand);
System.out.println("----------------------------------------");
//10
c.brand = "Dell";
System.out.println(c.brand);
System.out.println(stu.computer.brand);
System.out.println(stu.computer.brand == c.brand);
}
}
内存分配步骤(按代码):
public class Test {
public static void main(String[] args) {
Student stu = new Student();
-
public class Test
Java虚拟机(JVM)去方法区寻找是否有Test类的代码信息,如果存在,直接调用。如果没有,通过类加载器(ClassLoader)把.class字节码加载到内存中,并把静态变量和方法、常量池加载(“xiaoming”、“Hasse”)。 -
Student
以同样的逻辑对Student类进行加载;静态成员;常量池(“studying”)。 -
Student stu
,stu在main方法内部,因而是局部变量引用,存放在栈中。 -
new Student
,new出的对象(实例),把对象中的成员变量存放在堆中,以方法区的类信息为模板创建实例。 -
‘’=‘’赋值操作,把new Student的地址告诉stu变量,stu通过四字节的地址(十六进制),引用该实例。
-
stu.name = "xiaoming";
stu通过引用new Student实例的name属性,该name属性通过地址指向常量池的"xiaoming"。 -
stu.age = 10;
实例的age属性是基本数据类型,直接赋值。 -
stu.study();
调用实例的方法时,并不会在实例对象中生成一个新的方法,而是通过地址指向方法区中类信息的方法。
Computer c = new Computer();
同stu变量的生成过程。c.brand = “Hasse”
;同stu.name = "xiaoming"过程。
-
stu.computer = c;
该Student实例的computer属性指向该Computer类的实例。
-
c.brand = "Dell";
这时会创建一个新的字符串常量并指向“Dell”
如果我们创建一个字符串String str = "Dell"; System.out.println(c.brand == str);
会发现结果为true.根据常量池具有共享性,可知并不会生成新的常量"Dell",而是会把str通过地址指向原来的"Dell".