内存分区

栈空间(stack

  • 由系统自动分配,遵循后进先出的原则,用于存放局部变量
  • 每个线程私有,不能实现线程间的共享。
  • 速度快!栈是一个连续的内存空间。

堆空间(heap)

  • 用于存放new出的对象,或者说是类的实例。
  • 堆是一个不连续的内存空间,分配灵活,速度慢。

方法区(method)

  • 在堆空间内。
  • 被所有线程共享。
  • 用来存放程序中永远不变或唯一的内容,如:①类的代码信息;②静态变量和方法;③常量池(字符串敞亮等,具有共享机制)。

常量池

JVM 为每个已加载的类维护一个常量池,常量池就是这个类用到的常量的一个有序集合。

包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用。

池中的数据和数组一样通过索引访问。

由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。

常量池存在于方法区(Method Area)。

实例分析

创建两个实体类和测试类

  • 实体类 Student、Computer
package cn.share.vo;

public class Student {

private Integer score;
private Integer age;
private String name;
private Computer computer;

public void study() {
System.out.println("studying...");
}

/*Getter和Setter方法*/
package cn.share.vo;

public class Computer {

private Double price;
private String brand;

/*Getter和Setter方法*/
}
  • 测试类 Test
package cn.share;

import cn.share.vo.Computer;
import cn.share.vo.Student;

public class Test {
public static void main(String[] args){

Student stu = new Student();
stu.setName("xiaoming");
stu.setAge(10);
stu.study();

Computer c = new Computer();
c.setBrand("Hasse");
System.out.println(c.getBrand());

stu.setComputer(c);
System.out.println(stu.getComputer().getBrand());

System.out.println("================== 分割线1 ===================");

c.setBrand("Dell");
System.out.println(c.getBrand());
System.out.println(stu.getComputer().getBrand());

System.out.println(c.getBrand() == stu.getComputer().getBrand());

System.out.println("================== 分割线2 ===================");

String str = "Dell";
System.out.println(c.getBrand() == str);
}
}
  • 运行结果

代码分析

程序的入口是main(),因而从main方法从上到下、从左到右进行分析。

Student stu = new Student();

① 首先,Java虚拟机(JVM)去方法区寻找是否有 Test 类的代码信息,如果存在,直接调用。如果没有,通过类加载器(ClassLoader)把 .class 字节码加载到内存中,并把静态变量和方法、常量池加载(“ xiaoming ”、” Hasse ")

② 走到 Student,以同样的逻辑对 Student 类进行加载;静态成员;常量池(“studying”)。

③ 走到 stustumain 方法内部,因而是局部变量,存放在栈空间中。

④ 走到 new Studentnew 出的对象(实例),存放在堆空间中,以方法区的类信息为模板创建实例。

⑤ 赋值操作,把 new Student 的地址告诉 stu 变量,stu 通过四字节的地址(十六进制),引用该实例。

stu.setName(“xiaoming”);

stu通过引用new Student实例的name属性,该name属性通过地址指向常量池的"xiaoming"常量

stu.setAge(10);

stu实例的 age 属性是基本数据类型,基本数据类型直接赋值。

stu.study();

⑧ 调用实例的方法时,并不会在实例对象中生成一个新的方法,而是通过地址指向方法区中类信息的方法。

⑥⑦⑧的过程如下图:

Computer c = new Computer();

同stu变量的生成过程。

c.setBrand(“Hasse”);

同 stu.setName(“xiaoming”); 过程

stu.setComputer©;

⑨ 把 c 对象对 Computer 实例的引用赋值给 Student 实例的 computer 属性。亦即:该 Student 实例的computer 属性指向该 Computer 类的实例。

如下图:

拓展

改变brand的地址指向

重新将 Computer 实例的 brand 属性指向"Dell"常量,那 stu.computer.brand 指向谁呢?Dell 还是Hasse

c.setBrand("Dell");

根据刚才的分析可知:
stu 通过地址引用 Student 实例,而该实例的 computer 的指向和 c 的指向是同一个 Computer 实例,因而改变该 Computer 实例的 brand 属性的指向,两者都会改变。

举个例子:
访问小明,和访问小明的儿子的爸爸,实质上访问的是同一个对象:小明。

因而,最终如上图测试结果是 true。

理解字符串常量及常量池

下面我们添加新的代码,如下:

String str = "Dell";
System.out.println(c.getBrand() == str);

根据常量池具有共享性,可知并不会生成新的常量"Dell",而是会把 str 通过地址指向原来的 "Dell",因而结果是 true。