一文说清楚jvm 内存模型 & 栈上分配& 标量替换

今天简单讲一下jvm 内存模型(JDK1.8版本)

jvm 内存模型主要可以分为以下几个模块

  1. 堆内存
  2. 栈内存
  3. 本地方法栈
  4. 方法区
  5. 程序计数器

堆内存

​ 其实开发过程中我们多多少少都听说过:“我们创建出来的对象都在堆内存里面”,这个的确是没有错的,我们创建出的对象大多数都在堆内存里面,但是请注意,我说的是大多数对象,不是全部的对象,而且,堆内存也不仅仅是存放我们创建出来的对象,还有一些其他的信息,比如类的反射信息,没错,我们在用反射的时候,其实是有一个类的反射对象在堆内存的,我们调用反射方法其实就是调用者反射类的方法

栈内存

​ 栈内存,也叫线程栈,所谓线程栈,就是每个线程运行时所拥有的的一块专属的内存空间,jvm会在创建的线程时给线程分配,每个每个线程在执行方法的时候,又会给每个方法分配独立的内存空间,这块空间称之为栈帧,一个方法对应一个栈帧空间,栈帧空间与栈帧空间之间相互独立,并且存在当前线程栈之中,而栈帧里面又包括一些其他的信息,比如

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法出口

下面我就来讲一下每一块内存都是什么意思,有什么作用

局部变量表:

​ 就是用来存放当前方法内部的局部变量的

操作数栈:

​ 假如我在方法内申明了一个数字类型的变量,或者进行了数值方面的运算,这个时候就会用到操作数栈

动态链接:

​ 这个很难理解,我举个例子,我们都知道,java是一门支持多态的语言,子类可以重新父类的方法,而我们执行方法的时候,在编译期间是不知道具体执行的是子类的的方法还是父类的方法的,这个只有在运行的时候才能拿知道,这样的话,这个运行的方法就是动态链接,所谓动态链接,其实就是存放的具体执行方法的代码的地址

方法出口:

​ 方法出口很好理解,就是a方法调用了b 方法,b方法执行完毕值,要回到被调用的地方,这个时候会需要一个内存来存放这个代码的地址,这就类似于我们打游戏进入副本,刷完副本之后你会回到进入副本前的位置,方法出口就是记录这个进入副本前的位置的

本地方法栈:

​ 本地方法栈也很好理解,我们都知道java是1995年诞生的,在此之前基本都是C/C++的天下,很多东西都是C/C++实现的,所以我们java 在执行某些方法的时候,会调用C++ 代码(就是虚拟机目录下dll文件,调用的过程就类似于我们调用了一个第三方jar包),这个方法调用的过程被称之为调用本地方法,而本地方法执行所需要的内存都是在本地方法栈里面的

程序计数器:

​ 这个也是每一个线程独有的,java是一门多线程语言,每个线程可以独立运行代码,当我们线程a运行method方法的时候,运行到一半,时间片用完了,这个时候线程b也来运行method方法,也运行到一半运行完了,时间片回到线程a,这个时候,需要 一个内存空间来记录当前线程运行到哪一行代码了,这个程序计数器就是来记录运行代码的行记录

方法区:

​ 这个区域可能会存放好几块信息

  1. 常量池
  2. 代码元数据
  3. 静态变量
  4. klass对象
常量池:

很好理解,和名字一样,就是用来存放常量的,就是我们用final修饰的变量,当然,还有一种情况,我们string a=“abc”

这个abc 字符串也会放在常量里面,还有integer包装类 0-128 这几个数字也在常量,当然其他包装类型也有一些数据放在常量池里面,这里不展开讲,有兴趣评论区留言,到时候单独出一期来讲一讲

代码元数据:

这个就是我们写的代码,我们写的代码就是存放在这块空间的

静态变量:

就是被我们用static修饰的变量,这个也会被放在方法区

klass对象:

么错,就是klass 这个不是我们java的class对象,是jvm使用的klass对象,这个是jvm使用的,是C++ 的对象

例子

我上面讲可能比较抽象,下面我来举一个例子,如下代码

public Math {
	public static final String str="111";
	public Math math=new Math();
	public static Math math1=new Math();
	
	public void method(){
		int a=10;
		int b=20;
		int c=(a+b)*10;
		Math math4=new Math();
		String s=math.str;
		Math math5=math.math;
	}
	
	public Math method2(){
		Math math2=new Math();
		math2.str="100";
		return math2;
	}
	
	public static void main(String[]args){
		Math math3=new Math();
		math3.method();
		math3.hashcode();
		math3.method2();
	}
}

Math 类有普通放,一个main方法,有两个静态变量,一个普通变量

  1. 这些代码信息就是存放在我们的方法区里面的
  2. 两个静态变量,变量名存放在方法区,但是创建出来的对象放在堆内存,普通属性,也就事math属性,math 变量名称放在栈空间,而创建出来的对象放在堆空间
  3. 我们来运行代码,首先,我们运行main函数,jvm 会创建一个线程,这个线程我们称之为主线程,jvm会给主线程分配内存,也就是给主线程自己独立的 线程栈空间,然后主线程来运行main方法,这个时候,jvm会在主线程的线程栈里面划分出一块空间用来运行main方法,这个空间就是mian方法的栈帧空间,
    1. 我们来看第一行代码 Math math3=new Math(); 我们创建了一个对象,对象名称叫math3 ,按照上面的我说的逻辑,这个math3 会放在main栈帧空间的局部变量表里面,然后创建出来的这个对象放在对内存里面,注意,局部变量表里面的math3 其实存放的是 math3 这个对象的在堆内存的内存地址,
    2. 然后第二行代码,math3.method(); 我们执行了Math类的mathod 方法,这个时候,jvm又会在当前线程的线程栈空间里面划分出一个栈帧空间用来供 method方法 使用, 注意这个栈帧空间也是在当前线程栈里面,但是是和main方法线程栈隔开的,是相互独立
    3. 然后我们进入mathod方法第一行代码,int a=10; 这个时候,当前栈帧空间里面 的局部变量表里面已经有 三个局部变量了:a,b,c(这个局部变量在分配线程栈空间的时候就会存在,只不过那时候都是默认值,没有赋予真正的值),然后jvm 会进行赋值操作,jvm 会把 10 压入操作数栈,然后在把10 赋值给局部变量表里面的a ,这个时候 局部变量表里面的a 才会变成a=10
    4. 第二行代码和第一行一样,先把20 压入操作数栈,然后赋值给局部变量表里面的b
    5. 第三行代码,jvm 会吧 a的10 压入操作数栈(注意,上一步执行完赋值操作之后,操作数栈就空了),然后把b的20 压入操作数栈,然后把 10 和20 传给cpu ,cpu进行运行,得到30 ,然后把30 压回操作数栈,然后10 被压入操作数栈,然后10 和 30 被压入cpu,cpu运行得到300,300被压入操作数栈,然后300 被赋值给 局部变量表里面的c,这个时候c =300。
    6. 以上就是栈空间的局部变量表和操作数栈的用法
    7. 接着我们继续往下看,method方法执行完毕,方法返回mian,这个时候,jvm怎么知道需要返回到main方法的哪一行代码呢,这个时候时候,方法出口就排上用场了,method方法的栈帧的方法出口记录了mathod 方法运行完毕之后,jvm应该返回到调用mathod方法代码的具体位置,然后接着往下执行,这就是方法出口的作用
    8. 接下来执行math3.hashcode(); 注意,hashcode 是math父类object类方法,这个方法在调用的时候才知道具体执行的是父类的还是子类的方法,这种在运行期间才能确定的方法我们也需要用一块内存来记录,这块内存就是动态链接,动态链接具体存储的就是运行的具体代码的位置。
    9. hashcode 方法运行完毕之后,然后jvm运行下一行代码math3.method2();
    10. 其实mathod2 方法 和mathod 差不多,也是 局部变量存 栈的局部变量表里面,然后对象存在堆内存,这里不在重复,我这里说一个重点,就是他们的返回值,在mathod方法中是没有返回值的,也就是说在mathod创建的对象,他的作用域没有超过这个方法本身(这个过程被称为对象的逃逸分析,就是判断这个对象的作用范围有没有逃逸出这个方法本身),所以,在这种情况下,jvm其实不会把创建出来的对象放在堆内存,而是会直接放在栈内存,应为这样方法结束之后,栈帧空间被回收,对象也就是直接回收了,而如果放在堆内存里面,堆内存满了,需要做gc,gc会产生stw,这样影响性能。
    11. 除了对象栈上分配之外,其实jvm还会做一件事,就是标量替换,因为如果对象本身没有逃逸出这个方法,其实jvm在方法内部使用的的就是对象的属性和方法罢了,如果是属性,我直接给属性分配内存并且赋值就可以了,如果是方法,那么就再申请一块栈帧空间,这样的话,我连对象都不用创建的了(创建对象本身也需要 内存空间,在64位机器上开启指正压缩为16个字节,对象头8个字节,对象指针4个字节,对其填充位4个字节),所以其实连对象都不用创建,只要给对象的属性赋值就可以了,这个过程被称之为标量替换
posted @ 2023-01-28 23:24  刘阿泽  阅读(237)  评论(0编辑  收藏  举报