对象内存图
对象是 new 出来的, 对象中存放的是地址.
程序示例:
Javabean 类:
public class Phone {
// 属性 (成员变量)
String brand;
double price;
// 行为 (方法)
public void call() {
System.out.println("手机在打电话");
}
public void playGame() {
System.out.println("手机在玩游戏");
}
}
测试类:
public class PhoneTest {
public static void main(String[] args) {
// 创建手机对象
Phone p = new Phone();
System.out.println(p); // classes.Phone@776ec8df
}
}
new 一个对象要发生的事情至少包括以下七个步骤:
Student s = new Student(); // new 一个对象
-
加载 Student.class 字节码文件. 因为用到了这个类, 所以要加载这个类的字节码文件
-
申明等号左侧的局部变量
-
在堆内存中开辟一个空间
-
默认初始化
-
显示初始化 (在类的定义中给定初始值即为显式初始化)
-
构造方法初始化 (在类中定义了有参构造方法时执行这一步)
-
将堆内存中的地址值赋值给左边的局部变量
可以让两个变量指向同一个对象:
Javabean 类:
public class Student {
String name;
}
测试类:
public class StudentTest {
public static void main(String[] args) {
Student s1 = new Student();
s1.name = "小明";
Student s2 = s1;
System.out.println(s1.name + " " + s2.name); // 小明 小明
s2.name = "小张";
System.out.println(s1.name + " " + s2.name); // 小张 小张
s1 = null;
// System.out.println(s1.name); // Exception in thread "main" java.lang.NullPointerException: Cannot read field "name" because "s1" is null
System.out.println(s2.name); // 小张
}
}
一个对象的内存图 的分析过程:
下图 的代码中, 首先是将 TestStudent 类的字节码文件加载到方法区中, 并将 main() 方法在其中进行临时存储.
接下来, 虚拟机会自动调用程序的主入口 main() 方法, 于是, main() 方法被加载到栈里面.
然后开始执行 main() 方法里面的代码. 第一句是 Student s = new Student();, 创建一个 Student 类的对象 s.
创建这个对象时, 遵循上面所说的创建一个对象要经历的 7 个步骤.
第一步是将类 Student 的 class 文件加载到方法区, 且这个空间内有着这个类的全部信息, 比如所有的成员变量 name, age, 所有的成员方法 study() 等.
第二步是申明局部变量, 也就是创建对象这条语句 Student s = new Student(); 左边的代码 Student s, 在 main() 方法中就开辟了一个空间, 这个空间的名字就叫做 s. 这个空间可以存储 Student 这个类的对象的地址值.
第三步, 在堆内存中开辟一块空间, 也就是创建对象这条语句 Student s = new Student(); 右边的代码 new Student(). 堆内存里面的空间都是有地址值的, 假设这块空间的地址值为 001, 这块空间里面会把 Student 这个类的所有的成员变量都拷贝一份放过来 (从方法区中拷贝, 因为这些信息存储在方法区中) . 除此之外 001 这块空间还存储有 Student 这个类的所有成员方法的地址, 这个地址是为了以后用对象调用方法的时候能找到对应的方法, 等于说并没有复制成员方法, 成员方法还是在方法区中, 只是复制了成员变量到堆内存中. 此时这个地址为 001 的空间就是我们所说的对象. 但是这个对象现在还没有创建完毕. 因为此时的成员变量 name 和 age 都还没有值. 赋值操作就是接下来的 4, 5, 6 三个步骤.
第四步, 首先是默认初始化, name 默认初始化为 null, age 默认初始化为 0.
第五步, 是显示初始化, 如果 Student 类的定义中, 成员变量时直接给定了值, 即 String name = "zhangsan"; 和 int age = 10;, 那么这就叫做显示初始化, 此时上一步里面默认初始化的 null 和 0 在这一步中就会被 zhangsan 和 10 代替, 但是这里 Student 类并没有写这样的显示初始化的代码, 那么显式初始化可以忽略.
第六步, 是构造方法初始化, 在创建实例 s 时, 即 Student s = new Student(); 这条语句中, 小括号中什么都没写, 就表示现在调用的是空参构造, 且是虚拟机提供的默认的空参构造方法, 这个默认的空参构造方法的方法体是空的, 没有任何语句, 所以空参构造是可以忽略的. 但是如果此时用的是有参构造, 那么在构造方法初始化这一步, 就会有值代替默认初始化中的 null 和 0. 从此也可以看出, 构造方法只是创建对象的多个步骤中的一个步骤而已.
第七步, 即把堆内存中的地址赋值给左边的局部变量. 即把地址 001 通过等号运算符即赋值运算符赋值给了左边的变量 s. 此时 s 这个变量的空间里面就会存储地址值 001. 于是变量 s 也可以通过地址值 001 找到堆内存中的空间.
至此, 创建对象的这条语句, 即 Student s = new Student();, 就执行完了.
如果直接打印 s, 就是打印 s 中记录的地址值 001.
s.name 就表示 001 这个内存空间里面的 name, 就找到了堆内存里面的 name.
如果执行语句 s.name = "aqiang";, 则改变的也是堆内存里面的 name.
语句 s.study(); 就是找 001 这个空间里面的 study(), 但是找到的是这个成员方法的地址, 再用这个地址, 找到了方法区里面的成员方法 study(). 此时 study() 方法就会被加载进栈. study() 方法执行完毕后, 就会出栈. 此时整个 main() 方法就执行完毕了, 于是 main() 方法也出栈. 既然 main() 方法都出栈了, 那么 main() 里面的变量自然也就没有了. 于是 s 也就没有了, 于是也就没有变量再使用堆内存中的这块原本地址值为 001 的这一块空间了. 专业点说就是没有变量指向这块空间了, 那么这块空间也会消失.
两个对象的内存图 的分析过程:
创建第一个对象和第二个对象的过程和上述分析一个对象的内存图 的过程是一样的.
执行 Student s2 = new Student(); 语句时, Student.class 文件不需要再加载一次, 因为已经存在于方法区中了, 直接用就可以了.

浙公网安备 33010602011771号