刷题理解JVM内部的数据存储

       前言 

        逆水行舟,不进则退!!!     

        来实操!!!


       

目录

       判断变量的存储位置      

       常量池

       对象和引用

       静态变量

       类加载器

       内存溢出

       内存模型

       内存分配与垃圾回收


       判断变量的存储位置      
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 时, 它的对象变成了 不可达对象, 

        垃圾回收器会 对 不可达对象进行回收, 


        我是专注学习的章鱼哥~

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值