【JVM】实例分析Java代码运行时内存布局

时间:2023-01-02 16:32:26
Java内存模型对于我们实际分析Java代码有着无可替代的作用。用一个小例子来分析Java代码运行时,内存是如何布局的。
package test01;
//日期类
class BirthDate {
private int day;
private int month;
private int year;

public BirthDate(int d, int m, int y) {
day = d;
month = m;
year = y;
}

public void setDay(int d) {
day = d;
}

public void setMonth(int m) {
month = m;
}

public void setYear(int y) {
year = y;
}

public int getDay() {
return day;
}

public int getMonth() {
return month;
}

public int getYear() {
return year;
}

public void display() {
System.out.println
(day + " - " + month + " - " + year);
}
}

public class MemoryTest{
public static void main(String args[]){
MemoryTest test = new MemoryTest();
int date = 9;
BirthDate d1= new BirthDate(7,7,1970);
BirthDate d2= new BirthDate(1,1,2000);
test.change1(date);
test.change2(d1);
test.change3(d2);
System.out.println("date=" + date);
d1.display();
d2.display();
}

public void change1(int i){
i = 1234;
}

public void change2(BirthDate b) {
b = new BirthDate(22,2,2004);
}

public void change3(BirthDate b) {
b.setDay(22);
}
}

注分析内存,首先我们必须要知道,Java中的内存主要在栈和堆中进行。同时在创建实例(new)时,对象会被分配在堆内存中
为什么会在堆内存中创建实例对象呢?这是因为堆内存比较大,而且我们创建的对象大小是不定的,只有在Java运行时才能计算出真正的大小,
而堆内存可以根据对象的大小动态的为对象分配内存。

一、实例创建

现在根据main方法中执行的代码步骤来分析内存块的情况。首先看对象实例化的代码前四行:
      MemoryTest test = new MemoryTest();
int date = 9;
BirthDate d1= new BirthDate(7,7,1970);
BirthDate d2= new BirthDate(1,1,2000);

如图:
【JVM】实例分析Java代码运行时内存布局

首先会在栈中分别分配一块内存空间存放test,date,d1,d2,同时在堆内存中分配三块内存空间,分别存放test,d1,d2所创建的实例。如图。
由于date是一个int类型,属于Java基本类型,所以直接在栈内存中分配,不需要占用堆内存。
这里在创建BirthDate实例时,运用到了BirthDate的构造方法。构造方法在使用时,会在栈内存中给构造方法的变量分配空间,并且给他们赋值,
然后在堆内存中创建实例时,会将变量的值赋值给堆内存中创建的实例。当构造方法执行完成后,在栈内存为构造方法分配的变量会销毁。
这个时候第一阶段就完成了。
接下来看执行方法的部分。

二、change1方法

 test.change1(date);
在执行change1方法时,由于有局部变量i,所以会现在栈中为i分配一块儿空间,如图:

【JVM】实例分析Java代码运行时内存布局

接下来回去执行方法体,将i进行赋值为1234。

【JVM】实例分析Java代码运行时内存布局
当完成change1方法调用时,i在内存中的生命周期就结束了。会立即销毁。也就是说只要方法执行结束,在栈中的局部变量会立即销毁。
最后执行完change1方法后,内存又恢复到了最开始的样子。跟chang1没执行之前是一样的。
【JVM】实例分析Java代码运行时内存布局

二、change2方法

接下来开始执行
test.change2(d1);
首先在栈中创建change2方法的参数b变量,同时将d1的值赋给b,由于d1是引用类型,所以会将b的引用也指向d1的引用。如图:

【JVM】实例分析Java代码运行时内存布局

这里其实b和d1的值应该是完全一样的。只是为b这个变量分配一块儿空间,没有变量值可言。并且b和d1的值是完全一样的。
在执行方法体时b = new BirthDate(22,2,2004);又重复构造方法的过程,在堆中创建一个实例,将b的引用指向新的实例。

【JVM】实例分析Java代码运行时内存布局

指向新的实例后,b在栈空间的值就和之前的不一样了。hashcode就会发生改变。

在执行完change2方法后,b就会销毁。b指向堆中的实例不会立即销毁,需要垃圾回收机制来将它回收。

三、change3方法

test.change3(d2);
在执行change3方法时,也会在栈中创建一个b的变量,同时将d2的值复制给b,同时将b指向d2的实例。

【JVM】实例分析Java代码运行时内存布局

当执行完change3后d2指向的实例的day变为22。同时b会随之销毁。最后内存的布局回事这个样子的:
【JVM】实例分析Java代码运行时内存布局

小结

根据上述的过程,可以总结出局部变量会在栈内存中分配临时的空间来存储,当方法执行完成后,分配的临时空间会立即销毁。
同时指向堆中的实例也就无效了,等待GC进行回收。根据这点,我们为了程序的运行效率,在编写代码时,应该尽量少new对象实例,只在需要的时候才去new对象,否则当new出来的实例不再被用时,
由于GC会去检查堆中的实例在栈中有没有被引用,如果被引用则不能被回收。所以new出来的对象会永久的存放在内存中,这样长时间会影响内存的利用率和程序的运行效率。
最后总结一下内存各个区的主要职能:
栈:存放局部变量和对象的引用。
堆:对象的实例
方法区:静态变量和常量字符串
data:加载的代码class文件