前言
逆水行舟,不进则退!!!
来实操!!!
目录
判断变量的存储位置
public class Test {
private int x = 10;
public void method() {
int y = 20;
final int z = 30;
}
}
问: 变量 x, y, z 分别存储在什么位置?
变量 x : 这是一个实例变量, 实例变量存储在堆内存中, 随着对象的创建而分配内存, 每个Test类的对象都会有自己的一份 x 变量;
变量 y : 这是一个局部变量, 定义在方法 method 中, 局部变量存储在栈内存中. 每次调用 method 方法时, 都会在栈上创建新的 y 变量, 并分配内存.
变量 z : 这是一个 final 修饰的局部变量, 同样定义在方法 method 中, 尽管它是被 final 修饰的, 但它仍然是一个局部变量, 因此也存储在栈内存中, 它的 final 修饰符意味着它的值在初始化后不能被改变.
总结:
- 实例变量 x 存储在堆内存中。
- 局部变量 y 存储在栈内存中。
- final 局部变量 z 也存储在栈内存中。
常量池
public class Test {
public void method() {
String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
}
}
问:变量 str1、str2、str3 指向的对象存储在什么位置?"hello" 字面量存储在什么位置?
字符串字面量 "hello": 这个字符串字面量是被存储在运行时常量池中, 这个常量池位于堆内存中, 专门用于存储字符串字面量和常量. 这个" hello " 会在程序加载时被放入常量池中.
变量 str1: 首先这个变量是局部变量, 存储在栈内存中, 其次, 当使用 "hello" 初始化时, str1 会指向常量池中的那个 "hello" 对象.
变量 str2: 和 str1 一样, 也是局部变量, 也被初始化为字符串字面量 "hello" . 由于常量池中的字符串是唯一的, 所以 str2 也会指向常量池中的那个 "hello" 对象. 此时 str1 和 str2 指向同一个字符串对象, 存储在常量池中.
变量 str3 : 当使用 new 关键字创建字符串对象时, 它会创建一个新的 String 实例, 这个实例是存储在堆内存中的, 然后这个实例指向了常量池中的字符串字面量 "hello". str3 最终也是指向了常量池中的 "hello".
当执行 String str3 = new String("hello"); 时, 发生了以下情况:
1, 在堆内存中创建了一个新的 String 实例, 这个实例要存储 "hello" 字符串的引用.
2, 然后, 这个新的 String 实例检查字符串常量池中是否已经存在 "hello" 字符串.
如果常量池中不存在 "hello" 字符串, 则会在常量池中创建一个新的字符串对象, 并将这个引用传递给堆内存中的 String 实例 |
如果常量池中已经存在 "hello" 字符串, 则会直接引用常量池中的字符串对象. |
3, 最终, str3 这个变量指向这个在堆内存中的 String实例, 而这个实例又指向了字符串常量池中的 "hello" 字符串.
比较对象和内容
引用比较 (==):
str1 == str2 是 true,因为它们都指向常量池中的同一个对象。
str1 == str3 是 false,因为它们指向的是不同的对象(常量池中的对象 vs 堆内存中的对象)。
str2 == str3 也是 false,原因同上。
内容比较 (equals):
str1.equals(str2) 是 true,因为它们的内容相同。
str1.equals(str3) 也是 true,因为它们的内容相同。
str2.equals(str3) 同样是 true,因为它们的内容相同。
为什么不同?
常量池的优化:常量池的目的是为了优化内存使用。对于相同的字符串字面量,常量池只会保存一个对象,因此多个引用可以共享相同的对象,减少内存开销。
new 关键字的使用:使用 new 关键字明确表示创建一个新的对象。即使内容相同,每次调用 new 都会在堆内存中创建一个新的 String 对象。
是否可以更改?
字符串的不可变性:无论是常量池中的字符串还是堆内存中的字符串,String 对象在 Java 中都是不可变的。这意味着一旦创建,字符串的内容不能被修改。
抛出一个问题: str3是局部变量, 如果方法执行结束, 方法栈帧会被销毁, str3也会随之销毁, 但是str3指向的堆内存中的String对象会怎样? 会不会造成内存泄漏?
先简单说一下, 并不会造成内存泄漏, JVM中有专门的垃圾回收机制来进行处理, 等我相关博客写完了, 我会将链接放到这里.
总结:
常量池中的字符串对象是唯一且共享的,而 new 关键字创建的字符串对象是独立的,即使它们的内容相同。
这种不同是由于内存优化和对象创建方式的不同,而不是因为字符串对象是否可变。在 Java 中,所有字符串对象都是不可变的
对象和引用
public class Test {
public void method() {
Test t = new Test();
t.method();
}
}
问:Test 对象 t 和方法调用栈帧存储在什么位置?
分析:
当方法 method() 被调用的时候, 为该方法的执行创建一个新的栈帧, 在栈帧中创建了一个 名为 t 的局部变量.
在执行 new Test() 时, 会在堆内存中创建一个新的 Test 对象, 并将其地址赋值给 t 变量.
然后, 执行 t.method() 时, 会重复上述步骤, 这里是递归调用, 会不断的开辟栈帧, 直到栈空间溢出, 抛出 StackOverflowError 异常为止.
栈内存:
method方法第一次被调用
+------------------------+
| method 栈帧 |
| +--------------------+ |
| | Test t --------+ | |
| +--------------------+ |
+------------------------+
堆内存:
+-------------------------+
| Test 对象 |
| +---------------------+ |
| | (数据成员等) | |
| +---------------------+ |
+-------------------------+
首先, 堆内存用于存储所有的对象实例和数组, ( 除了字符串 ) , 那问题来了 : 为什么这些对象实例都存储在堆内存中?
堆内存的特点是全局访问, 并且对象的生命周期不受限于方法栈帧. 这使得堆内存中的对象可以在多个方法调用之间共享, 并且它们的生命周期由垃圾回收器管理,
所以将对象存储在堆内存中更符合要跨方法, 跨线程的特性.
数组为什么也是存储在堆内存中?
1, 在Java中, 数组被视为对象,
2, 生命周期: 数据是需要被跨方法调用的, 存储在堆内存中支持这样的需求;
3, 灵活分配: 堆内存支持动态内存分配, 适合处理大小不同的数组;
4, 内存容量: 堆内存容量更大, 更适合数组, 如果存储在栈中可能会导致栈溢出.
总结:
Test 对象 t 存储在栈内存中, 指向堆内存中的一个 Test 对象.
方法调用会在栈内存中创建一个新的栈帧, 用于存储方法的局部变量和执行方法.
静态变量
public class Test {
private static String staticStr = "static";
public void method() {
String localStr = "local";
}
}
问:静态变量 staticStr 和局部变量 localStr 分别存储在什么位置?
静态变量 staticStr :
属于类, 存储在方法区中;
在 Test 类加载时, 静态变量 staticStr 会被加载到方法区
局部变量 localStr :
该变量在方法中声明, 是局部变量, 在方法被调用的时候创建, 存储在栈帧中.
当 method 方法被调用时, 会为该方法的执行创建一个新的栈帧, 其中包含局部变量localStr.
在 method() 方法执行结束后, 局部变量 localStr 所占用的栈帧空间会被释放.
总结:
静态变量 staticStr存储在方法区中, 局部变量 localStr 存储在方法调用的栈帧中.
类加载器
public class Test {
static {
System.out.println("Static block");
}
public static void main(String[] args) {
Test t = new Test();
}
}
问:静态代码块和类的元数据(如方法、字段信息)存储在什么位置?
代码中, 类的元数据包括: 类的名称 Test, 类的修饰符 public , 静态代码块, main 方法.
类的元数据在类加载的时候, 会被加载到方法区或元空间
什么是元空间? ( 这个问题可以先略过, 不是这片博客的则重点 )
元空间是 Java8 虚拟机规范中引入的一个新概念,取代了早期 Java 虚拟机中的永久代。
元空间不再是 JVM 堆内存的一部分,而是使用本地内存(native memory)来存储类的元数据信息。
( 永久代的大小在JVM运行后是固定的, 无法在运行时进行动态调整, 只能在JVM启动前通过两个参数来调整初始大小和最大空间. ) 当应用程序加载大量类或者动态生成大量的类时( 永久代的方法区存储的是类的元数据: 包括类的结构, 方法, 字节,常量池等), 可能会导致永久代空间不足, 从而导致OOM, 触发FullGC.
GC效率也是用元空间来替换永久代一个重要因素, 在永久代中, 类的元数据信息和常量池等都是由垃圾收集器进行管理的, 但永久代的垃圾回收机制是和Major(老年代)绑到一块儿的, 老年代大家都不陌生, 是通过FullGC来处理的, 这两个任何一个空间满了,都会触发FullGC, 这就导致FullGC的频繁触发, 影响应用的性能. 而元空间是使用本地内存类存储类的元数据信息, 不再受到垃圾收集器的管理, 因此减少了FullGC的频率.
元空间的大小不受 JVM 的堆大小限制,而是受系统可用内存大小限制。
元空间的默认大小可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 参数来指定初始大小和最大大小。
元空间的优势在于可以动态扩展,不再有方法区的永久代大小限制问题,但也需要注意避免本地内存耗尽的问题。
内存溢出
public class Test {
public void recursiveMethod() {
recursiveMethod();
}
public static void main(String[] args) {
Test t = new Test();
t.recursiveMethod();
}
}
问:描述该程序在运行过程中可能出现的内存溢出情况,并指出它们涉及到的存储区域
我不清楚是不是有人和我一样, 在该类中实例化该类这里犯迷糊, 其实在Test类中不论是new 一个Test类, 还是new 一个其他类, 其实都是一样的, 都是创建一个新的 类对象的引用, 即使是新new 出来的Test对象, 也和现在的Test对象没太大的关系.
这段代码会导致栈内存溢出, 当 main方法调用 recursiveMethod()方法的时候,, 会在栈上创建一个新的方法栈帧, 由于recursiveMethod 方法内部又调用了自身, 因此会反复的创建新的方法栈帧, 导致栈空间不断增加, 直到OOM.
内存模型
public class Test {
public static void main(String[] args) {
int[] arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
}
问:数组 arr 和其元素存储在什么位置?i 变量存储在什么位置?
数组 arr:
数据 arr 是存储在堆内存中的, 因为数组是一个对象.
arr 数组中的元素存储在数组对象所在的堆内存中, 这些元素是数组对象的一部分, 他们的值被存储在堆内存中的连续空间中.
i 变量:
i 变量存储在栈内存中.
内存分配与垃圾回收
public class Test {
public void method() {
Test t1 = new Test();
Test t2 = new Test();
t1 = null;
System.gc();
}
}
问:t1 和 t2 对象的内存分配与垃圾回收过程是怎样的?
分析:
在这个过程中, 首先在方法中new了两个类对象, 这两个对象是存储在堆内存中的, 他们的引用分别交给t1 和 t2, 然后t1 断开了对象的引用, 使得 ti 对象成为了不可达对象, 接着调用sSystem.gc() 方法 请求JVM执行垃圾回收, 虽然调用 该方法 不能保证 立即执行垃圾回收, 但会提醒JVM执行垃圾回收, JVM会在适当的时候进行垃圾回收操作.
在执行垃圾回收操作的过程中, JVM会标记并回收不可达对象, 因为 t1 曾指向的Test对象现在已经没有引用指向它, 所以会被标记为不可达对象, 最终被垃圾回收器回收, t2仍然保持着对 它的Test对象的引用, 因此不会被回收.
所以:
t1 和 t2 对象都被分配到了堆内存中,
在 t1 设置为 null 时, 它的对象变成了 不可达对象,
垃圾回收器会 对 不可达对象进行回收,
我是专注学习的章鱼哥~