深入浅出-JVM中堆和栈

1、JVM中的堆栈(1.8)

1.1 JVM内存模型

在这里插入图片描述
参考文档:
官网文档网址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
https://baijiahao.baidu.com/s?id=1698998030180488662&wfr=spider&for=pc
https://blog.csdn.net/u014781844/article/details/106903564

1.2 堆栈的比较

在这里插入图片描述

2、new对象的时候发生了什么

Java在new一个对象的时候:
1、会先查看对象所属的类有没有加载到内存中
2、如果没有,就会先通过类的全限定名来加载
3、加载并初始化完成后,再进行对象的创建工作
①类加载过程
通过双亲委派模型进行类的加载,先将请求传送给父类加载器,如果父类无法完成这个加载请求,子加载器才会尝试自己去加载。初始化也是先加载父类后加载子类。最终方法区会存储静态变量、类初始化代码、实例变量、实例初始化代码、实例方法等。
②创建对象
在堆中开辟对象的所需的内存。然后对实例变量和初始化方法进行执行。还需要在栈中定义了类引用变量,然后将堆内对象的地址赋值给引用变量。

2.1 对象详解(第一次使用该类)

详见:https://www.cnblogs.com/JackPn/p/9386182.html

2.2 举个例子

/**
 * 个人信息
 */
public class Person {
    int age;
    String name;
    public void walk(String name){
        System.out.println(name + "正在走路!");
    }
}
/**
 * 对象创建过程
 */
public class NewObject {
    public static void main(String[] args) {
        String newObject = "简单的对象创建过程!";
        System.out.println(newObject);
        Person person = new Person();
        person.name = "张三";
        person.age = 20;
        person.walk(person.name);

    }
}

在这里插入图片描述

3、 值引用和对象引用的区别

  • java只有值传递 本质都是值的拷贝(内存地址本身也是一种数值)
    • 值传递:传递的是值的拷贝
    • 引用传递:传递的是对象的内存地址的拷贝

4、Code Demo

4.1 一个实例对象可以有多个引用变量

    /**
     * 一个实例对象可以有多个引用变量
     * ps:对象的内存地址伴随每次gc或者是重新启动都会改变
     */
    @Test
    public void getAddress() {
        //pa为引用变量,存储在栈中;new Person为实例对象,存储在堆中
        Person pa = new Person("张三", 18, "中国");
        //打印引用对象pa指向的地址A
        System.out.println("pa第一次获取地址A的值(内存地址)为:" + VM.current().addressOf(pa));
        //打印地址A实际存储的实例对象信息
        System.out.println("pa地址A存储的对象信息为:" + pa);
        //Java中的对象是JVM在管理,JVM会在她认为合适的时候对对象进行移动,比如,在某些需要整理内存碎片的GC算法下发生的GC,从而会导致对象的地址变动,这里我们手动gc下
        System.gc();
        System.out.println("pa第二次获取地址A的值(内存地址)为:" + VM.current().addressOf(pa));

        Person pb = pa;
        //打印引用对象pb指向的地址A
        System.out.println("pb第一次获取地址A的值(内存地址)为:" + VM.current().addressOf(pb));
        //打印地址A实际存储的实例对象信息
        System.out.println("pb地址A存储的对象信息为:" + pb);
        if (VM.current().addressOf(pa) == VM.current().addressOf(pb)) {
            System.out.println("pa和pb为同一个对象");
        }
        //pa第一次获取地址A的值(内存地址)为:31898735320
        //pa地址A存储的对象信息为:Person(name=张三, age=18, country=中国)
        //pa第二次获取地址A的值(内存地址)为:29033054880
        //pb第一次获取地址A的值(内存地址)为:29033054880
        //pb地址A存储的对象信息为:Person(name=张三, age=18, country=中国)
        //pa和pb为同一个对象
    }

4.2 基本数据类型比较

    /**
     * 基本数据类型比较
     */
    @Test
    public void intCompare() {
        int a = 3;
        int b = 3;
        int c = a;
        System.out.println(VM.current().addressOf(3));
        System.out.println(VM.current().addressOf(a));
        System.out.println(VM.current().addressOf(b));
        System.out.println(VM.current().addressOf(c));
        if (a == b && b == c) {
            System.out.println("都相等");
        }
    }

4.3 值传递:基本数据类型

    /**
     * 值传递:基本数据类型
     * 引用变量和数值都存储在栈中,数值的内存地址是不变的,比如3-31868843208,4-31868843224
     * ps:数值的内存地址,伴随每次启动基本不会改变,除非手动触发gc
     */
    @Test
    public void testInt() {
        int a = 3;
        System.out.println("a的当前值:" + a);
        System.out.println("a的内存地址:" + VM.current().addressOf(a));
        add(a);
        System.out.println("第一自增后,a的当前值:" + a);
        System.out.println("第一自增后,a内存地址:" + VM.current().addressOf(a));
        //手动触发gc
//        System.gc();
        a = add(a);
        System.out.println("第二自增后,a返回后的当前值:" + a);
        System.out.println("第二自增后,a返回后的内存地址:" + VM.current().addressOf(a));
        //a的当前值:3
        //a的内存地址:31868843208
        //a自增前的当前值:3
        //a自增前的内存地址:31868843208
        //a自增后的当前值:4
        //数字4的内存地址:31868843224
        //a自增后的内存地址:31868843224
        //第一自增后,a的当前值:3
        //第一自增后,a内存地址:31868843208
        //a自增前的当前值:3
        //a自增前的内存地址:31868843208
        //a自增后的当前值:4
        //数字4的内存地址:31868843224
        //a自增后的内存地址:31868843224
        //第二自增后,a返回后的当前值:4
        //第二自增后,a返回后的内存地址:31868843224
    }

    /**
     * 数值+1
     *
     * @param a
     * @return
     */
    public int add(int a) {
        System.out.println("a自增前的当前值:" + a);
        System.out.println("a自增前的内存地址:" + VM.current().addressOf(a));
        a = a + 1;
        System.out.println("a自增后的当前值:" + a);
        System.out.println("数字4的内存地址:" + VM.current().addressOf(4));
        System.out.println("a自增后的内存地址:" + VM.current().addressOf(a));
        return a;
    }

4.4 简单的引用地址传递-1

    /**
     * 简单的引用地址传递-1
     * 传参的时候会把引用地址copy一份给方法参数
     */
    @Test
    public void testString() {
        //name指向的是方法区-常量池中"张三"的内存地址A
        String name = "张三";
        //调用方法时,会把name存储的内存地址copy一份赋值给参数变量str,此时name和str都指向"张三"
        //执行方法内容后,str重新赋值并指向"李四",而name的指向依旧不变还是"张三"
        changeValue(name);
        System.out.println(name);
        //张三
    }

    /**
     * 将值修改为:"李四"
     *
     * @param str
     */
    public void changeValue(String str) {
        str = "李四";
    }

4.5 简单的引用地址传递-2

    /**
     * 简单的引用地址传递-2
     */
    @Test
    public void testStringBuilder() {
        StringBuilder sb = new StringBuilder("王五");
        //调用方法时,会把sb存储的内存地址copy一份赋值给参数变量stringBuilder,此时sb和stringBuilder都指向"王五"
        //执行方法后,改变所指向对象的值"王五"+"赵六",这里需要注意引用地址值没变,改变的是对象的值
        changeSbValue(sb);
        System.out.println(sb);
        //王五赵六
    }

    /**
     * 增加"赵六"
     *
     * @param stringBuilder
     */
    public void changeSbValue(StringBuilder stringBuilder) {
        stringBuilder.append("赵六");
    }

4.6 思考:为啥一个对象执行完后,要对引用变量设置=null?

    /**
     * 思考:为啥一个对象执行完后,要对引用变量设置=null?
     * 为了gc,当引用变量设置为空,可以是原先引用的对象没有引用,根据gc可达性分析的原则,当对象不存在引用时,会被回收
     */
    @Test
    public void setNull() {
        Person person = null;
        System.out.println("未new时的内存地址:" + VM.current().addressOf(person));
        person = new Person("张三", 30, "中国");
        System.out.println("设null前的内存地址:" + VM.current().addressOf(person));
        person = null;
        System.out.println("设null后的内存地址:" + VM.current().addressOf(person));
        //未new时的内存地址:0
        //设null前的内存地址:31904171312
        //设null后的内存地址:0
    }

4.7 思考:final关键字修饰的变量一旦被赋值后,还有修改吗?

    /**
     * 思考:final关键字修饰的变量一旦被赋值后,还有修改吗?
     * 这里需要理解一个概念:修饰的变量不可变,注意这里的不可变是指引用不可变,值是可变的
     */
    @Test
    public void setFinal() {
        //创建了一个引用age指向20这个值
        final int age = 20;
        //创建了一个引用name,指向常量池"张三"这个值,其中"张三"这个常量值是不可改变的
        final String name = "张三";
        //age = 32; //能修改成32吗
        //name = "李四"; //能修改成李四吗?
        final Person person = new Person("张三", 30, "中国");
        System.out.println("person修改前内容:" + person);
        System.out.println("person内容修改前的内存地址:" + VM.current().addressOf(person));
        person.setCountry("深圳");
        System.out.println("person修改后内容:" + person);
        System.out.println("person内容修改后的内存地址:" + VM.current().addressOf(person));
        //person修改前内容:Person(name=张三, age=30, country=中国)
        //person内容修改前的内存地址:31898614976
        //person修改后内容:Person(name=张三, age=30, country=深圳)
        //person内容修改后的内存地址:31898614976
    }

4.8 思考::java中的基本数据类型一定存储在栈中吗?

 首先说明,"java中的基本数据类型一定存储在栈中的吗?”这句话肯定是错误的。
 下面让我们一起来分析一下原因:
 基本数据类型是放在栈中还是放在堆中,这取决于基本类型在何处声明,下面对数据类型在内存中的存储问题来解释一下:
 一:在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因
 在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。
 (1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈中
 (2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆类存中的。
 二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。
 同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量
  (1)当声明的是基本类型的变量其变量名及其值放在堆内存中的
  (2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中
 此外,为了反驳观点" Java的基本数据类型都是存储在栈的 ",我们也可以随便举出一个反例,例如:
 int[] array=new int[]{1,2};
 由于new了一个对象,所以new int[]{1,2}这个对象时存储在堆中的,也就是说1,2这两个基本数据类型是存储在堆中,
 这也就很有效的反驳了基本数据类型一定是存储在栈中~~

4.9 探索:在-128——127的值存在栈中,超出-128——127这个范围的值是存哪?

    int i1 = 1;
    int a1 = -128;
    int b1 = -129;
    int c1 = 127;
    int d1 = 128;
    /**
     * 思考:在-128——127的值存在栈中,超出-128——127这个范围的值是存哪?根据测试结果推测在堆中
     */
    @Test
    public void setLocalVar() {
        //范围内取值 每次重启内存地址值不变,除非手动gc
        int i2 = 1;
        int i4 = 1;
        System.out.println("每次重启值不变,i1=:" + VM.current().addressOf(i1));
//        System.gc();
        System.out.println("每次重启值不变,i2=:" + VM.current().addressOf(i2));
        System.out.println("每次重启值不变,i3=:" + VM.current().addressOf(1));
        System.out.println("每次重启值不变,i4=:" + VM.current().addressOf(i4));
        //起始边界值 每次重启值不变
        int a2 = -128;
        System.out.println("每次重启值不变,a1=:" + VM.current().addressOf(a1));
        System.out.println("每次重启值不变,a2=:" + VM.current().addressOf(a2));
        System.out.println("每次重启值不变,a3=:" + VM.current().addressOf(-128));
        //结束边界值 每次重启值不变
        int c2 = 127;
        System.out.println("每次重启值不变,c1=:" + VM.current().addressOf(c1));
        System.out.println("每次重启值不变,c2=:" + VM.current().addressOf(c2));
        System.out.println("每次重启值不变,c3=:" + VM.current().addressOf(127));
        //超出起始边界值 每次重启值都会改变
        int b2 = -129;
        System.out.println("每次重启值都会改变,b1=:" + VM.current().addressOf(b1));
        System.out.println("每次重启值都会改变,b2=:" + VM.current().addressOf(b2));
        System.out.println("每次重启值都会改变,b3=:" + VM.current().addressOf(-129));
        int b4 = -129;//b2和b4的内存地址值不一样,可以初步判断每次都是在堆中创建一个新的对象空间
        System.out.println("每次重启值都会改变,b4=:" + VM.current().addressOf(b4));
        //超出结束边界值 每次重启值都会改变
        int d2 = 128;
        System.out.println("每次重启值都会改变,d1=:" + VM.current().addressOf(d1));
        System.out.println("每次重启值都会改变,d2=:" + VM.current().addressOf(d2));
        System.out.println("每次重启值都会改变,d3=:" + VM.current().addressOf(128));
    }

5、思考

待补充

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值