初识Java内存——Java内存图(二)

有了上一篇文章的基础,这里我们再来做一些巩固练习,方便读者理解内存图的工作机理

例一

为例方便读者理解,这里我们用上一篇文章的例子稍作改进

1.代码

public class Test {
    //属性
    static int xx = 20;
    int yy = 200;
    String zz = new String("hello");
    //方法
    public static void main(String[] args) {
        System.out.println(xx);
        System.out.println(yy);
        System.out.println(zz);

        change();
        eat();

        Test test = new Test();
        change();
        test.eat();


    }

    public static void change(){
        System.out.println("change执行");
    }

    public void eat(){
        System.out.println("eat方法执行");
    }
}

这里请读者思考几个问题

xx与yy、change和eat方法有什么区别?映射到内存中要如何解释?

代码在编译器中会报错,你能从内存的角度解析问什么会报错吗?

请你自己尝试画内存图解释代码

2.解析

在这段代码里,xx 属于静态变量,而 yy 是实例变量,这是它们的主要区别。静态变量 xx 归类本身所有,它会被存放在方法区,并且所有类的实例都共享这一个变量。实例变量 yy 则不同,它属于类的实例,在创建对象时,会在堆内存里为其分配空间。

 

静态方法 change() 和实例方法 eat() 也存在明显差异。静态方法可以直接通过类名来调用,它不依赖于类的实例,而且在静态方法内部不能直接访问实例变量和实例方法。实例方法 eat() 必须通过类的实例来调用,在实例方法内部能够访问实例变量和静态变量。

 

下面从内存的角度来分析代码报错的原因:

 
  1. 在静态方法 main 里直接访问实例变量 yy 和 zz 是不允许的。这是因为静态方法在类加载的时候就已经存在于方法区了,此时还没有创建对象,也就不存在实例变量。实例变量是在对象创建后存放在堆内存中的,所以静态方法无法直接访问它们。
  2. 代码中静态变量 xx 可以正常访问,这是符合静态成员访问规则的。
  3. 静态方法 change() 能在 main 方法中直接调用,这是因为静态方法可以通过类名直接调用,也可以在同一个类的静态方法内部直接调用。
  4. 实例方法 eat() 在第一次调用时没有通过实例来调用,这就导致了编译错误。第二次调用时使用 test.eat(),通过创建的实例来调用,这种方式是正确的。
 

接下来分析代码各部分在内存中的存储位置:

 
  • 静态变量 xx:存放在方法区,在类加载的时候就会被初始化,并且只有一份拷贝。
  • 实例变量 yy 和 zz:存放在堆内存中,会随着对象的创建而产生,每个对象都有自己独立的实例变量。
  • 静态方法 main 和 change():它们的字节码存放在方法区,在类加载时就已经存在。
  • 实例方法 eat():其字节码同样存放在方法区,不过需要通过对象实例来调用。
  • 局部变量 test:存放在栈内存中,它保存的是堆内存中 Test 对象的引用。
 

要修正代码中的错误,就需要在访问实例变量和实例方法之前先创建对象实例。具体来说,在 main 方法中访问 yyzz 和调用 eat() 方法时,都要通过 Test 类的实例来进行。

3.内存图

此图只做参考,希望能帮助读者进一步理解

例二

通过上面的例子我们发现示例对象会存储在堆区中,那么思考一下示例对象之间会不会互相影响?

1.代码

public class Test {
    //属性
    static int xx = 20;
    int yy = 200;
    String zz = new String("hello");
    //方法
    public static void main(String[] args) {

        Test test = new Test();
        System.out.println(test.xx);
        System.out.println(test.yy);
        System.out.println(test.zz);
        

        System.out.println("--------------------");
        Test test1 = new Test();
        System.out.println(test1.xx);
        System.out.println(test1.yy);
        System.out.println(test1.zz);

        test1.xx = 1111;
        test1.yy = 222;
        test1.zz = new String("demo");

        System.out.println("===================");
        System.out.println(test.xx);
        System.out.println(test.yy);
        System.out.println(test.zz);
        System.out.println(test1.xx);
        System.out.println(test1.yy);
        System.out.println(test1.zz);
    }

    public static void change(){
        System.out.println("change执行");
    }

    public void eat(){
        System.out.println("eat方法执行");
    }
}

2.解析

  • 静态变量 xx

    • 归属:属于类本身,而非实例。
    • 内存位置:存储在方法区(Method Area),类加载时初始化,全局唯一。
    • 访问方式:可通过类名直接访问(Test.xx),也可通过实例访问(如 test.xx),但本质上所有实例共享同一副本。
  • 实例变量 yy 和 zz

    • 归属:属于类的实例(对象)。
    • 内存位置:存储在堆内存(Heap)中,每个对象独立拥有一份。
    • 访问方式:必须通过对象实例访问(如 test.yy)。
  • 静态方法 change()

    • 调用方式:通过类名直接调用(Test.change()),无需创建实例。
    • 内存位置:方法字节码存储在方法区。
    • 访问限制:只能访问静态成员(静态变量 / 方法),不能直接访问实例成员。
  • 实例方法 eat()

    • 调用方式:通过对象实例调用(如 test.eat())。
    • 内存位置:方法字节码存储在方法区,但需通过实例的 this 引用调用。
    • 访问权限:可访问静态成员和实例成员(通过 this

  • 方法区存储:
    • 类的结构信息(字段、方法定义)。
    • 静态变量 xx 初始化为 20
    • 所有方法的字节码(包括 main()change()eat())。
  • 栈内存
  • 存储局部变量 test,指向堆中的对象实例。
  • 存储局部变量 test1,指向堆中的另一个对象实例。
  • 堆内存:创建 Test 对象,包含:
    • 实例变量 yy = 200
    • 实例变量 zz 指向字符串常量池中的 "hello"(通过 new String("hello") 创建)。
  • 静态变量 xx:在方法区被修改为 1111,所有实例(包括 test)的 xx 均变为 1111
  • 实例变量 yy 和 zz:仅修改 test1 对象的副本,test 对象不受影响。
代码元素内存位置说明
静态变量 xx方法区类加载时初始化,全局唯一,所有实例共享。
实例变量 yy 和 zz堆内存每个对象独立拥有,创建对象时初始化。
静态方法 main()change()方法区类加载时存在,通过类名或实例调用(静态方法推荐类名调用)。
实例方法 eat()方法区类加载时存在,但需通过实例调用(隐式传递 this 引用)。
局部变量 testtest1栈内存存储对象引用,指向堆中的实例。
字符串常量 "hello""demo"字符串常量池(方法区)通过字面量创建的字符串存储在此,相同字面量共享同一地址。

3.内存图

例三

看完同一个类的实例创建,接下来我们来看在一个类中实例化其他类的对象

1.代码

public class Cat {
    public static int count=10;
    public String name = new String("猫");
    public int age = 2;

    public static void run(){
        System.out.println("run");
    }
    public void eat(){
        System.out.println("eat");
    }
}
public class Test1 {
    public static void main(String[] args) {
        Cat.run();
        System.out.println(Cat.count);
        System.out.println("=======");

        Cat cat1 = new Cat();
        cat1.eat();
        cat1.run();
        System.out.println(cat1.age);
        System.out.println(cat1.name);


        System.out.println("=======");
        Cat cat2 = new Cat();
        cat2.eat();
        cat2.run();
        cat2.name = "豆豆";
        cat2.age = 5;
        System.out.println(cat2.age);
        System.out.println(cat2.name);
    }

}

2.解析

静态变量 xx

  • 归属:属于类本身,而非实例。
  • 内存位置:存储在方法区(Method Area),类加载时初始化,全局唯一。
  • 访问方式:可通过类名直接访问(Test.xx),也可通过实例访问(如 test.xx),但本质上所有实例共享同一副本。

实例变量 yy 和 zz

  • 归属:属于类的实例(对象)。
  • 内存位置:存储在堆内存(Heap)中,每个对象独立拥有一份。
  • 访问方式:必须通过对象实例访问(如 test.yy)。

静态方法 change()

  • 调用方式:通过类名直接调用(Test.change()),无需创建实例。
  • 内存位置:方法字节码存储在方法区。
  • 访问限制:只能访问静态成员(静态变量 / 方法),不能直接访问实例成员。

实例方法 eat()

  • 调用方式:通过对象实例调用(如 test.eat())。
  • 内存位置:方法字节码存储在方法区,但需通过实例的 this 引用调用。
  • 访问权限:可访问静态成员和实例成员(通过 this)。

方法区

  • 类的结构信息(字段、方法定义)。
  • 静态变量 xx(初始值 20,修改后 1111)。
  • 所有方法的字节码(main()change()eat())。
  • 字符串常量 "hello""demo"(存储于字符串常量池)。

栈内存

  • 局部变量 testtest1:存储对象引用,分别指向堆中的 Test 实例。

堆内存

  • 对象实例 test
    • 实例变量 yy = 200
    • 实例变量 zz 指向字符串常量池中的 "hello"
  • 对象实例 test1
    • 初始时:yy = 200zz 指向 "hello"
    • 修改后:yy = 222zz 指向新字符串 "demo"

总结表格

代码元素内存位置说明
静态变量 count方法区类加载时初始化为 10,所有实例共享。
实例变量 name 和 age堆内存每个对象独立拥有,创建对象时初始化。
静态方法 run()方法区通过类名或实例调用,不能访问实例成员。
实例方法 eat()方法区通过实例调用,可访问静态和实例成员。
局部变量 cat1cat2栈内存存储对象引用,指向堆中的实例。
字符串常量 "猫""豆豆"字符串常量池(方法区)通过字面量创建的字符串存储在此,相同字面量共享同一地址。

3.内存图

例四

读者请保证自己能理解掌握前三个例子,再来看这个例子

1.代码

public class Cat {
    public static int count=10;
    public String name = "猫";
    public int age = 2;

    public static void run(){
        System.out.println("run");
    }
    public void eat(){
        System.out.println("eat");
    }
}
package com.qcby.MemeryMap;

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

        int a = 10;
        int b = 20;
        System.out.println(a);
        System.out.println(b);
        change1(a,b);
        System.out.println(a);
        System.out.println(b);
        System.out.println("===========");

        String a1 = "a1";
        String b1 = "b1";
        System.out.println(a1);
        System.out.println(b1);
        change2(a1,b1);
        System.out.println(a1);
        System.out.println(b1);
        System.out.println("===========");


        Cat a2 = new Cat();
        Cat b2 = new Cat();
        b2.name = "豆豆";
        b2.age = 5;
        System.out.println(a2);
        System.out.println(b2);
        change3(a2,b2);
        System.out.println(a2);
        System.out.println(b2);
    }

    public static void change1(int a,int b){
        int tmp = a;
        a = b;
        b = tmp ;
    }

    public static void change2(String a,String b){
        String tmp = a;
        a = b;
        b = tmp ;
    }

    public static void change3(Cat a,Cat b){
        Cat tmp = a;
        a = b;
        b = tmp ;
    }

    public static void change4(Cat a,Cat b){
        int x1 = a.age;
        a.age = b.age;
        b.age = x1;
        String str = a.name;
        a.name = b.name;
        b.name = str;
    }

}

读到这里的读者请完成这三件事
1.预测一下代码的结果
2.复制代码到编译器上看看结果跟自己预期的是否一致

3.画出内存图来解释,编译器输出的结果

2.解析

读者运行代码后发现肯定发现了,只有change4方法成功交换了对象的值
为什么前三个方法没有交换成功?
你能不能自己分析出来?
下面请看作者的参考解析

代码解析

核心逻辑

  1. 基本类型参数传递(change1

    • 方法接收 int 类型参数,尝试交换值,但基本类型按值传递,原变量不受影响。
  2. 字符串参数传递(change2):

    • 方法接收 String 类型参数(引用类型),但字符串不可变,交换引用不会影响原变量指向的字符串常量。
  3. 对象引用传递(change3

    • 方法接收 Cat 对象引用,交换引用变量指向的对象,但原变量的引用未被修改(仅在方法内临时交换)。
  4. 对象属性修改(change4

    • 方法通过引用直接修改对象属性(age 和 name),原对象的属性会被永久改变。

内存分析

方法区

  1. 类结构信息

    • Test1 类字节码(包含 mainchange1-change4 方法)。
    • Cat 类字节码(包含 nameage 实例变量和默认构造器)。
  2. 字符串常量池

    • "a1""b1" 存储于常量池,由 a1b1 引用。

栈内存

  1. main 方法栈帧

    • 基本类型变量
      • a = 10b = 20(存储于栈帧局部变量表)。
    • 引用类型变量
      • a1 引用字符串常量池中的 "a1"
      • b1 引用字符串常量池中的 "b1"
      • a2b2 分别引用堆中的 Cat 对象实例(地址 1、地址 2)。
  2. 方法调用栈帧

    • change1 栈帧
      • 接收 a=10b=20 的副本,交换后局部变量 a=20b=10,不影响 main 中的原变量。
    • change2 栈帧
      • 接收 a1b1 的引用副本,交换后副本指向 "b1""a1",但原变量 a1b1 仍指向原值。
    • change3 栈帧
      • 接收 a2b2 的引用副本,交换后副本指向对方对象,但原变量 a2b2 仍保持初始引用。
    • change4 栈帧
      • 接收 a2b2 的引用副本,通过引用直接修改对象属性(堆中数据),原对象属性永久改变。

堆内存

  1. Cat 对象实例
    • a2 指向的对象
      • name = "猫"(默认值,来自 Cat 类定义)。
      • age = 2(默认值)。
    • b2 指向的对象
      • name = "猫"
      • age = 2
    • 调用 change4 后
      • a2 对象:name = "猫"(假设 b2 对象修改后的值)、age 与 b2 交换。
      • b2 对象:name 与 a2 交换、age 与 a2 交换。

代码元素内存位置说明
基本类型变量 ab栈内存存储整数数值 1020,方法调用时按值传递副本,原变量不受修改影响。
字符串变量 a1b1栈内存存储引用,指向方法区字符串常量池中的 "a1""b1"
Cat 对象引用 a2b2栈内存存储引用,分别指向堆中的 Cat 实例(地址 1、地址 2)。
静态方法 change1-change4方法区类加载时存储字节码,通过类名调用,change3/change4 接收对象引用副本。
Cat 实例变量 nameage堆内存每个 Cat 对象独立拥有,初始值为 "猫"2change4 中被永久修改。
字符串常量 "a1""b1"字符串常量池(方法区)字面量创建的字符串,a1b1 直接引用,不可变。
Cat 类定义方法区存储类结构信息(字段、默认构造器),类加载时初始化。
Test1 类定义方法区存储 main 方法及静态方法字节码,类加载时初始化。

3.内存图

上面的解释很啰嗦吧,接下来作者会画的细一点,注意方法内的指向(黄色),以及主方法(main)中(红色)的数值/引用指向


change1() //main方法中还存储了其他变量,为了方便理解,我这里省略了,只画相关的变量。

从图中可以看出,交换数值确实发生了,但发生在change1()内的空间,主方法空间内位发生改变,所以在主线程打印的值未发生变化

change2()

与change1()同理

change3()

change4()

我们来解析为什么change4()能成功交换

change4() 方法之所以能够成功交换两个 Cat 对象的属性(age 和 name),而其他方法(如 change3())无法改变原对象的引用或属性,核心原因在于 Java 的参数传递机制 和 对象引用的操作方式

拓展

有些细节不知道读者是否发现

String a = new String("a");
String b = "b";

读者是否有关注过这两个变量存储的位置?

a是直接存储在堆内存中的
b是存储在堆内存的字符串常量池
究其原因是
 

(1) 字面量 "a" 的存储

  • 存储位置字符串常量池(方法区)

  • 说明
    当使用 new String("a") 时,JVM 首先会检查字符串常量池中是否已存在字面量 "a"

    • 若不存在,则在字符串常量池中创建 "a" 的实例。

    • 若已存在,则直接引用常量池中的 "a"

(2) 通过 new 关键字创建的 String 对象

  • 存储位置堆内存

  • 说明
    new String("a") 会在堆内存中创建一个新的 String 对象,该对象内部持有对字符串常量池中标量 "a" 的引用(通过 char[] 数组指向常量池中的字符序列)。

    • 即使常量池中已有 "a"new 操作也会在堆中创建独立的对象实例。

    • 此时,变量 a 存储在栈内存中,指向堆中的 String 对象。

(3) 字面量 "b" 的存储

  • 存储位置字符串常量池(方法区)

  • 说明
    当使用字面量直接赋值(如 String b = "b";)时,JVM 会直接在字符串常量池中查找是否存在 "b"

    • 若不存在,则在常量池中创建 "b" 的实例,并将变量 b(存储在栈内存中)直接指向常量池中的该实例。

    • 若已存在,则直接让 b 指向常量池中的现有实例(避免重复创建,节省内存)。

 核心区别总结

创建方式内存位置是否创建新对象内存复用性
new String("字面量")堆内存(对象)+ 常量池(字面量)是(堆中新建对象)常量池字面量复用,堆对象不复用
字面量直接赋值(如 "b")字符串常量池否(直接引用常量池)复用常量池已有实例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值