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. 堆栈内存按照线程粒度划分区域。
比如分为:主线程、新线程、其他线程等。
2. 每个线程区域按照方法粒度划分区域。
例如:主线程区域分为一个区域:main(); 新线程分为两个区域:()、run()
3. 每个方法区都包含其所有局部变量。
例如,main()方法区包含局部变量:[] args,,int z = 30,
这里可以看到[] args,Fruit Fruit,这三个局部变量只存储变量,而不存储变量的数据,而int z = 30既存储了变量int z,又存储了变量30的数据。这证实了上面第一个结论,“在栈内存中,基本数据类型的变量和变量的数据是存储在一起的”。 这里注意:它只是说它们是一起存储的,但并没有说它们在栈内存中是一起存储的。 也可能一起在堆内存或者方法区,继续分析。
方法区:
1、方法区按照类粒度划分区域。 (所以方法区中存储的内容只加载一次)
例如:Fruit.class、.class、Fruit$1.class(这是什么?稍后解释)
2. 在每个类区域中包含所有静态变量。 (所以方法区中存储的内容只加载一次)
例如:Fruit.class 包含 int x = 10, 。 基本数据类型再次出现在这里。 就像在堆栈内存中一样,基本数据类型的变量与变量的数据存储在一起。 与堆栈内存的唯一区别是局部变量位于堆栈内存中,而静态变量位于方法区中。 多变的。
堆:
1. 堆内存根据实例及其包含的非静态变量划分为区域。
例如:
1、new[]对应的变量在栈内存中:[]args;
2、new()+int = 10对应的变量在方法区: ;
3、new()+int = 30对应的变量在栈内存中:;
4. new Fruit()+int y = 20+ 对应的变量在栈内存中:;
5、new()+int = 20对应的变量在堆内存中:;
6. new Fruit$1() 没有对应的变量,因为它是匿名的,并且它的类文件 Fruit$1.class 存在于方法区中(这是什么,见下文)
我一直不明白 Fruit$1.class 是什么。 我们看一下编译后的文件:
其实就是一个类。 。 。 仅仅因为我们使用了匿名对象,就为我们生成了这样的东西。 如果我们将 new 放入另一个类中并尝试,会发生什么情况? 新建Test.class如图:
为了便于介绍,我对对象中的所有内容进行了注释。 编译后我们得到:
打开看看:
果然是一个类,所以匿名对象实际上会生成一个class文件。 类名由匿名对象所在的类的名称,后跟 $ 和数字组成。 原谅我的基本辣鸡。 。 。
好吧,我们重新描述一下上面的堆内存第六项,说一下:
6. new Fruit$1()(即new())因为是匿名的,所以没有对应的变量,其类文件Fruit$1.class存在于方法区中。
现在更容易理解了。
当我们回顾 Fruit$1.class 文件时,我们可以看到它有一个构造函数:
水果$1(水果){}
所以堆内存中的形式应该是new Fruit$1()(即new())+Fruit,其中Fruit变量对应的实例是堆中的第4项。
注意⚠️:
相同颜色代表变量和对象之间的引用关系
由于方法区和堆内存中的数据在线程之间共享,因此线程Main、New和都可以访问方法区中的静态变量并访问该变量引用的对象的实例变量。
栈内存中的每个线程都有自己的虚拟机栈,每个栈帧之间的数据对于该线程来说是唯一的。 也就是说,线程New中的方法无法访问线程Main中的局部变量,但是我们发现“fruit”,它也是Main的局部变量,被访问了。 为什么是这样? 因为“水果”已被宣布为最终的。
当“fruit”被声明为final时,“fruit”将作为New构造函数的参数传递给New。 也就是说,堆内存中的Fruit$1对象中的实例变量val$fruit将引用“fruit”所引用的对象。 因此New可以访问Main的局部变量“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.方法区:保存各个加载的类的信息; 该信息是类加载器在加载类时从类的源文件中提取的; 变量信息也保存在方法区中;
5、基本数据类型:Java的八种基本数据类型。
6、参考数据类型:Java类类型、接口类型、数组类型。
其中,枚举类型和注解类型属于特殊的类类型;
字符串是数组类型(因为底层是char数组);
7、成员变量:在类中的方法(代码块)之外定义的变量。
使用修饰的成员变量:静态成员变量位于方法区(详细信息见下文),并且处于类级别。
未修改的成员变量:实例成员变量位于堆空间(具体见下文),是对象级别的。
8、局部变量:除成员变量外,所有局部变量都位于方法所在的栈帧中(具体见下文),并且是方法级别的。
2、变量的存储方法
1. 局部变量
局部变量是在调用方法时执行初始化语句时创建的。
作用范围是从初始化到方法(或语句块)结束。
基本类型的局部变量:调用方法时执行变量的初始化语句时,变量的值直接保存到方法的栈帧中。
引用数据类型局部变量:当方法被调用时,引用值在堆内存中的地址被保存到变量所在的栈内存中。
上述方法执行后,堆空间中的对象失去引用,等待GC自动回收。
2、成员变量
实例成员变量:
实例成员变量是在使用new关键字创建对象时创建的,并具有默认初始值(基本类型对应基本类型的初始值,引用类型的默认值为null)。
局部变量没有初始值,必须在使用之前进行初始化。
上图中对象的实例成员变量是i和stu,它们位于堆空间中。 Stu是一个引用类型的成员变量,它又引用堆空间中的另一个对象。 p 是 main 方法中的局部变量。
静态成员变量(类成员变量):
类成员变量在加载字节码文件时加载。 当 JVM 将 .class 加载到内存中时,静态成员变量就存在。 与字节码一样,它们位于称为方法区的内存空间中。 类成员变量 由该类的所有对象共享。
3、总结(表)
变量类型
代码中的位置 生命周期开始 生命周期结束 内存中的位置
局部变量
在方法、方法参数和代码块中
执行初始化语句时
方法代码块末尾
堆栈空间
实例成员变量
在班上
当对象被创建时
对象被GC回收
堆空间
类成员变量
课堂上(使用修改)
当类的字节码加载到内存中时
JVM 停止运行
方法区