JVM基础(十二):Java中局部变量、实例变量和静态变量在方法区、栈内存、堆内存中的分配

目录

1、java中的变量与数据类型

2、思考

3、例子:

3.1、分析:

3.2、结论


1、java中的变量与数据类型

变量是一个容器,用来装什么的呢?装内存地址,这个内存地址对应的是内存中某个数据。
那为什么这个容器不直接装这个数据更简洁呢?因为直接装数据的话,这个数据就无法被别的变量使用,无法复用就会导致很多不便。
所以变量的内存分配可以看成两个不部分:1、变量在内存中的分配(“变量分配”) 2、变量所引用的数据在内存中的分配(“数据分配”)

1.1、变量类型有:

局部:在方法内声明的变量
实例:在类中但在方法外声明并且没有static
静态:声明为static的变量

1.2、变量的数据类型有:

1、原始数据类型 2、引用数据类型

2、思考

原始数据类型变量的“变量分配”和“数据分配”是在一起的(都在方法区或栈内存或堆内存,这里思考什么时候在方法区、什么时候在栈内存、什么时候在堆内存???)
引用数据类型的“变量分配”和“数据分配”不一定是在一起的(什么情况是在一起的?什么情况不在一起?)

3、例子:

public class Fruit {

 private static int x = 10;
 static BigWaterMelon bigWaterMelon_1 = new BigWaterMelon(x);

 private int y = 20;
 private BigWaterMelon bigWaterMelon_2 = new BigWaterMelon(y);

public static void main(String[] args) {
    Fruit fruit = new Fruit();
    int z = 30;
    BigWaterMelon bigWaterMelon_3 = new BigWaterMelon(z);


    new Thread() {
        @Override
        public void run() {
            int k = 100;
            setWeight(k);
        }

        void setWeight(int waterMelonWeight) {
            fruit.bigWaterMelon_2.weight = waterMelonWeight;
            System.out.printf(fruit.bigWaterMelon_2.weight + "");
        }

    }.start();
}
}

class BigWaterMelon {

 int weight;

 BigWaterMelon(int weight) {
    this.weight = weight;
 }
}

3.1、分析:

栈:

1、栈内存中按线程粒度来划分区域。

比如划分为:主线程、new Thread线程、其他线程等等

2、每个线程区域中又按方法粒度来划分区域。

比如:主线程区域中划分了一块区域:main();new Thread线程中划分了两块区域:setWeight()、run()

3、每块方法区域中包含其所有的局部变量。

比如main()方法这块区域中包含局部变量:String[] args、Fruit fruit、int z = 30、BigWaterMelon bigWaterMelon_3
这里可以看到String[] args、Fruit fruit、BigWaterMelon bigWaterMelon_3这三个局部变量在这里只存储了变量,而没有存放变量的数据,而int z = 30既存放了变量int z 也存放了变量的数据30。这就可以肯定上面的第一条结论“栈内存中,基本数据类型的变量和变量的数据是存放在一起的”,这里要注意:只是说存放在一起,但没说是一起在栈内存,也可能一起在堆内存或者方法区,继续往下分析。

方法区:

1、方法区中按class粒度来划分区域。(所以方法区存储的都是只加载一次的)

比如:Fruit.class、BigWaterMelon.class、Fruit$1.class(这是个什么玩意?后面解释)

2、在每块class区域中包含其所有的静态变量。(所以方法区存储的都是只加载一次的)

比如:Fruit.class中包含static int x = 10、static BigWaterMelon bigWaterMelon_1。这里又出现了基本数据类型,和在栈内存中一样,基本数据类型的变量和变量的数据存放在一起,和栈内存中唯一不同的是,栈内存中是局部变量,而方法区中是静态变量。

堆:

1、堆内存中按实例和其所包含的非静态变量划分区域。

比如:
1、new String[] 对应的变量是栈内存中的 : String[] args;
2、new BigWaterMelon()+int weight = 10 对应的变量是方法区中 :static BigWaterMelon bigWaterMelon_1;
3、new BigWaterMelon()+int weight = 30 对应的变量是栈内存中 :BigWaterMelon bigWaterMelon_3;
4、new Fruit()+int y = 20+BigWaterMelon bigWaterMelon_2 对应的变量是 栈内存中 :Fruit fruit;
5、new BigWaterMelon()+int weight = 20 对应的变量是同在堆内存中的 : BigWaterMelon bigWaterMelon_2;
6、new Fruit$1() 没有对应变量,因为它是匿名的,在方法区中存在它的类文件Fruit$1.class(这是个啥东西往下看)

前面一直搞不明白Fruit$1.class这是个什么玩意,找编译后的文件看一下:

其实就是一个Thread类。。。只是因为我们用了匿名对象,所以给我们生成了一个这么个玩意,那如果我们把new Thread放在别的类中试试会怎么样,新建一个Test.class如图:

为了简介,我把Thread对象中所有东西都注释了。在编译后得到:


打开看看:


果然就是一个Thread类,所以匿名对象其实会生成一个class文件,类名就是由匿名对象存在的类名字后面加$和数字拼成。原谅我的基础辣鸡。。。
好了,重新描述一下上面堆内存的第六条就可以这么说:
6、new Fruit$1()(即new Thread()) 没有对应变量,因为它是匿名的,在方法区中存在它的类文件Fruit$1.class。
这下就好理解了。
我们在回头看Fruit$1.class文件,可以看到他有一个构造函数:


Fruit$1(Fruit paramFruit) {}
所以在堆内存中的形式应该是new Fruit$1()(即new Thread())+Fruit paramFruit,其中Fruit paramFruit变量对应的实例就是堆中的第4条。

注意⚠️:

同一种颜色代表变量和对象的引用关系

由于方法区和堆内存的数据都是线程间共享的,所以线程Main Thread,New Thread和Another Thread都可以访问方法区中的静态变量以及访问这个变量所引用的对象的实例变量。

栈内存中每个线程都有自己的虚拟机栈,每一个栈帧之间的数据就是线程独有的了,也就是说线程New Thread中setWeight方法是不能访问线程Main Thread中的局部变量bigWaterMelon_3,但是我们发现setWeight却访问了同为Main Thread局部变量的“fruit”,这是为什么呢?因为“fruit”被声明为final了。

当“fruit”被声明为final后,“fruit”会作为New Thread的构造函数的一个参数传入New Thread,也就是堆内存中Fruit$1对象中的实例变量val$fruit会引用“fruit”引用的对象,从而New Thread可以访问到Main Thread的局部变量“fruit”。
 

3.2、结论

3.2.1、“变量的分配”:

局部变量在栈内存,静态变量在方法区,实例变量在堆内存。 也就是三个内存中都有变量。

3.2.2、“数据的分配”:

原始数据类型跟随自己的“变量分配”在一起,相亲相爱。
引用数据类型在堆内存中。

4、补充

栈空间:由JVM自动分配释放的一块内存空间 ,用于存放函数的参数值,局部变量值等,其工作原理遵循数据结构中栈”后进先出“的原则。

方法的栈帧:在方法被调用时JVM在栈空间的栈顶为此方法分配一块内存空间(该空间也称为:该方法的栈帧)(入栈),其中存放着该方法的数据信息(局部变量等),当该方法执行完毕,会自动将此栈帧弹出(出栈),其中的数据信息会被自动销毁。

1、解释说明

*测试代码如下:

public class test {
    public static void main(String[] args) {
        System.out.println("main方法操作一");    
        function();
        System.out.println("main方法操作二");
    }
    private static void function(){
        int a=1;
        System.out.println("function方法"+a);    
    }
}

2、画图分析执行流程以及栈帧的作用

3、堆空间:由程序员分配释放,创建对象就是为对象分配堆空间,当对象失去引用时,可能由Java的GC垃圾回收器回收。

4、方法区:保存在着被加载过的每一个类的信息;这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;static变量信息也保存在方法区中;

5、基本数据类型:Java的八大基本数据类型。

6、引用数据类型:Java的类类型、接口类型、数组类型。

其中枚举类型和注解类型属于特殊的类类型;
字符串属于数组类型(因为String底层就是一个char数组);
7、成员变量:定义在类中方法(代码块)外的变量。

   使用static修饰的成员变量: 静态成员变量位于方法区中(详细请看下文),是类级别的。
   未使用static修饰的成员变量:实例成员变量位于堆空间中(详细请看下文),是对象级别的。
8、局部变量 :除成员变量外都是局部变量位于方法所在的栈帧中(详细请看下文),是方法级别的。

二、变量的存储方式

1、局部变量
局部变量在方法被调用时执行到初始化语句时被创建。
作用区域为初始化后到方法(或语句块)结束。
基本类型的局部变量:在方法调用时执行到该变量的初始化语句时直接将该变量的值保存到该方法的栈帧中。

在这里插入图片描述

引用数据类型局部变量:在方法调用时将引用的堆内存中的值的地址保存到该变量到的栈内存中。

在这里插入图片描述

上面的方法执行完毕后,堆空间中的Person对象就失去了引用,等待着GC自动回收。
2、成员变量
实例成员变量:
实例成员变量在使用new关键字创建对象时被创建并带有默认的初始值(基本类型对应基本类型的初始值,引用类型默认值为null)。

局部变量是没有初始值的,必须初始化后才能使用。

在这里插入图片描述
上图中Person对象的实例成员变量是i和stu位于堆空间中,stu为引用类型成员变量,又引用了另一个堆空间中的对象,p为main方法中的局部变量。

静态成员变量(类成员变量):
类成员变量随着字节码文件的加载而被加载,当JVM将Person.class加载进内存时静态成员变量就存在了,和字节码一样,位于一块叫做方法区的内存空间中,类成员变量被该类的所有对象所共享。

在这里插入图片描述

三、总结(表格)

变量类型

代码中的位置生命周期开始生命周期结束内存中的位置
局部变量方法中,方法形参,代码块中执行到初始化语句时所在的方法代码块结束时栈空间
实例成员变量类中对象创建时对象被GC回收堆空间
类成员变量类中(使用static修饰)所在类的字节码被加载进内存时JVM停止运行时方法区
  • 6
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值