【JVM】详解JVM的五大内存模型、可能出现的异常以及堆栈引用易错点


在这里插入图片描述
源自:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 周志明

1、堆(线程共享)

Java 堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 里“几乎”所有的对象实例都在这里分配内存。

Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩
展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再
扩展时,Java虚拟机将会抛出OutOfMemoryError异常。原因有二:

  1. JVM堆内存设置不够。可以通过-Xms、-Xmx来调整。
  2. 代码中创建了大量大对象,并且不能被垃圾收集器收集(存在被引用)

2、方法区(线程共享)

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占有的内存。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

3、虚拟机栈(线程私有)

Java虚拟机栈(Java Virtual Machine Stacks)它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

这里需要关注一下入栈的过程:
如果方法里声明了基本数据类型(byte、short、int、long、double、float、char、boolean)的变量,那么它们都存储在栈中;如果方法中new了新的对象,那么会先去堆中创建该对象,然后栈中存储该对象的引用地址。如果方法中引用了已创建的对象,那么栈中存储该对象的引用地址。

如果某个线程的线程栈的内存被耗尽,没有足够的内存资源去创建栈帧,就会发生内存溢出。
例如如下代码:

public class Test {
    public static void m2(){
        m2();
    }
    public static void main(String[] args) {
        m2();
    }
}

上面这串代码的执行过程是:线程先执行main方法,同时会创建main方法的栈帧插入到该线程的线程栈中,当执行到m2()方法时,创建m2()方法的栈帧插入到该线程的线程栈中,执行到m2()方法里的m2()方法时,创建栈帧,插入到线程栈中,后面进行无脑创建栈帧、入栈。当创建一定数量的栈帧后,剩下的线程资源无法再创建新的栈帧
就会报StackOverflowError异常(堆栈溢出异常)(当前虚拟机栈不可以动态扩展)
异常截图:
在这里插入图片描述
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

4、本地方法栈(线程私有)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

5、程序计数器(线程私有)

程序计数器(Program Counter Register)也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。程序计数器的作用是记录当前线程下一条要运行的指令,这样保证了线程在切换回来时能回到正确的位置继续开始执行。

6、易错点

  1. 根据方法区中的类型信息去创建对象时,该类的静态属性不会出现在新创建的对象中,原因是对于类来说,每个静态属性只存在一份,不属于该类的某个对象。所以当你去打印一个新创建的对象时,只会打印出非静态的属性的值
public class UserParam {

    public static int a=0;

    private String userName;

    private String nickName;

    private UserParam userParam;

    public int getTest() {
        return test;
    }

    public void setTest(int test) {
        this.test = test;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    private int test;

    public UserParam getUserParam() {
        return userParam;
    }

    public void setUserParam(UserParam userParam) {
        this.userParam = userParam;
    }

    @Override
    public String toString() {
        return "UserParam{" +
                "userName='" + userName + '\'' +
                ", nickName='" + nickName + '\'' +
                ", userParam=" + userParam +
                ", test=" + test +
                '}';
    }
}

public static void main(String[] args) {
    UserParam userParam = new UserParam();
    UserParam.a=2;
    UserParam userParam1 = new UserParam();
    System.out.println(userParam);
    System.out.println(userParam1);
}

打印结果如下:

UserParam{userName='null', nickName='null', userParam=null, test=0}
UserParam{userName='null', nickName='null', userParam=null, test=0}
  1. 栈帧中的基本数据类型变量,只要赋值了,除非再次对其进行赋值,否则值不会改变。
public class Test {

    public static void main(String[] args) {
       UserParam userParam  = new UserParam();
       UserParam userParam1 = new UserParam();
       userParam1.setTest(userParam.getTest());
       System.out.println(userParam1);
       userParam.setTest(10);
       System.out.println(userParam1);
    }

}

打印结果:

UserParam{userName='null', nickName='null', userParam=null, test=0}
UserParam{userName='null', nickName='null', userParam=null, test=0}
  1. 引用类型会根据引用数据的改变而改变。
public class Test {

    public static void main(String[] args) {
       UserParam userParam  = new UserParam();
       UserParam userParam1 = new UserParam();
       userParam1.setUserName("aaaaa");
       func(userParam,userParam1);
        System.out.println(userParam);
    }
    public static void func(UserParam userParam,UserParam  userParam1){
        userParam.setUserParam(userParam1);
        userParam1.setUserName("bbbbbbbb");
    }

}

打印结果:

UserParam{userName='null', nickName='null', userParam=UserParam{userName='aaaaa', nickName='null', userParam=null, test=0}, test=0}
UserParam{userName='null', nickName='null', userParam=UserParam{userName='bbbbbbbb', nickName='null', userParam=null, test=0}, test=0}

可以看到,随着userParam1中userName的改变,userParam中的userParam也变了。原因是栈帧中引用类型变量存储的是堆中实例对象的地址,当实例对象改变,也意味着引用类型变量改变。

  1. 包装类型有拆装箱的过程,取值情况与基本数据类型一样
    在这里插入图片描述
public class Test {

    public static void main(String[] args) {
        UserParam userParam = new UserParam();
        userParam.setTest(10);
        UserParam userParam1 = new UserParam();
        userParam1.setTest(userParam.getTest());
        userParam.setTest(100);
        System.out.println(userParam1);
    }

}

打印结果如下:

UserParam{userName='null', nickName='null', userParam=null, test=10}
  1. jdk1.8中,String存在于堆中的字符串常量中,也是个对象。(堆的唯一目的就是用来存放实例对象)
public class Test {

    public static void main(String[] args) {
        UserParam userParam  = new UserParam();
        userParam.setUserName("yhz");
        UserParam userParam1 = new UserParam();
        userParam1.setUserName(userParam.getUserName());
        userParam.setUserName("aaaa");
        System.out.println(userParam1);
    }

}

打印结果:

UserParam{userName='yhz', nickName='null', userParam=null, test=null}

原因是:

  1. 创建完userParam对象,在给userParam设置userName为“yhz”时,会先去堆中的字符串常量池中创建“yhz”这个实例,然后将"yhz"实例的地址返回给userParam的userName。
  2. 创建完userParma1对象,在给userParma1设置userName为userParam.getUserName()时,userParam.getUserName()返回的userParam中userName保存的字符串常量池中"yhz"实例的地址,于是userParam1中userName指向字符串常量池中"yhz"实例(保存了字符串常量池中"yhz"实例的地址)。
  3. 当给userParam设置userName为"aaaa"时,会先去堆中的字符串常量池中创建“aaaa”这个实例,然后将"aaaa"实例的地址返回给userParam的userName,最后userParam的userName指向了"aaaa"然而userParam的userName还是指向"yhz"。

关于字符串常量池的一些内幕

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值