对象内存图

对象是 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 一个对象
  1. 加载 Student.class 字节码文件. 因为用到了这个类, 所以要加载这个类的字节码文件

  2. 申明等号左侧的局部变量

  3. 在堆内存中开辟一个空间

  4. 默认初始化

  5. 显示初始化 (在类的定义中给定初始值即为显式初始化)

  6. 构造方法初始化 (在类中定义了有参构造方法时执行这一步)

  7. 将堆内存中的地址值赋值给左边的局部变量

可以让两个变量指向同一个对象:

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() 方法在其中进行临时存储.


图 1

接下来, 虚拟机会自动调用程序的主入口 main() 方法, 于是, main() 方法被加载到栈里面.


图 2

然后开始执行 main() 方法里面的代码. 第一句是 Student s = new Student();, 创建一个 Student 类的对象 s.

创建这个对象时, 遵循上面所说的创建一个对象要经历的 7 个步骤.

第一步是将类 Student 的 class 文件加载到方法区, 且这个空间内有着这个类的全部信息, 比如所有的成员变量 name, age, 所有的成员方法 study() 等.


图 3

第二步是申明局部变量, 也就是创建对象这条语句 Student s = new Student(); 左边的代码 Student s, 在 main() 方法中就开辟了一个空间, 这个空间的名字就叫做 s. 这个空间可以存储 Student 这个类的对象的地址值.


图 4

第三步, 在堆内存中开辟一块空间, 也就是创建对象这条语句 Student s = new Student(); 右边的代码 new Student(). 堆内存里面的空间都是有地址值的, 假设这块空间的地址值为 001, 这块空间里面会把 Student 这个类的所有的成员变量都拷贝一份放过来 (从方法区中拷贝, 因为这些信息存储在方法区中) . 除此之外 001 这块空间还存储有 Student 这个类的所有成员方法的地址, 这个地址是为了以后用对象调用方法的时候能找到对应的方法, 等于说并没有复制成员方法, 成员方法还是在方法区中, 只是复制了成员变量到堆内存中. 此时这个地址为 001 的空间就是我们所说的对象. 但是这个对象现在还没有创建完毕. 因为此时的成员变量 name 和 age 都还没有值. 赋值操作就是接下来的 4, 5, 6 三个步骤.


图 5

第四步, 首先是默认初始化, name 默认初始化为 null, age 默认初始化为 0.


图 6

第五步, 是显示初始化, 如果 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 找到堆内存中的空间.


图 7

至此, 创建对象的这条语句, 即 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 的这一块空间了. 专业点说就是没有变量指向这块空间了, 那么这块空间也会消失.


图 8 完整的示意图

图 9 main 方法执行结束

两个对象的内存图 的分析过程:

创建第一个对象和第二个对象的过程和上述分析一个对象的内存图 的过程是一样的.

执行 Student s2 = new Student(); 语句时, Student.class 文件不需要再加载一次, 因为已经存在于方法区中了, 直接用就可以了.


图 10 完整的示意图

图 11 main 方法执行结束

图 12

图 13

图 14 main 方法执行结束
posted @ 2024-09-04 00:06  有空  阅读(50)  评论(0)    收藏  举报