首页 > 系统相关 >一文说清楚jvm 内存模型 & 栈上分配& 标量替换

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

时间:2023-01-28 23:35:20浏览次数:39  
标签:对象 栈上 线程 内存 jvm 标量 方法 Math

今天简单讲一下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个字节),所以其实连对象都不用创建,只要给对象的属性赋值就可以了,这个过程被称之为标量替换

标签:对象,栈上,线程,内存,jvm,标量,方法,Math
From: https://www.cnblogs.com/liouzeshuen/p/17071524.html

相关文章

  • JVM-字符串底层实现原理
    1.什么字符串会进入字符串常量池1.直接写的字面量2.字面量的拼接结果(注意:如果字符串拼接中有变量则结果不会进入字符串常量池)3.调用String的intern方法可以将String存入......
  • JVM垃圾回收
    一、如何判断对象可以回收?1.引用计数法:只要一个对象被其他变量所引用,就让计数加一,引用两次就让计数变为2,取消引用就让计数减一,但是这种算法有严重的弊端,如果两个变量......
  • 【已解决】Error: could not open `C:\Program Files\Java\jre1.8.0_121\lib\amd
    原因:java升级时,会在环境变量的path路径中增加以下两条路径,与我们安装java的路径重合。解决方法:删掉环境变量中的这两条语句,同时找到对应文件,删除即可。C:\ProgramData\O......
  • 精华推荐 | 【JVM深层系列】「GC底层调优系列」一文带你彻底加强夯实底层原理之GC垃圾
    前提介绍很多小伙伴,都跟我反馈,说自己总是对JVM这一块的学习和认识不够扎实也不够成熟,因为JVM的一些特性以及运作机制总是混淆以及不确定,导致面试和工作实战中出现了很多的纰......
  • synchronized加锁对象和JVM对锁的优化
    synchronized加锁对象对于普通同步方法,锁是调用该方法的对象。对于静态同步方法,锁是该方法所属类的Class对象。对于同步代码块,锁是synchronized括号里面的对象。JVM对锁......
  • JVM内存划分
    JVM内存划分概述java虚拟机(JavaVirtualMachine简称JVM):是运行所有java程序的抽象计算机,是java语言的运行环境。对于java语言来说,在虚拟机的自动内存管理机制的帮助......
  • 在windows系统中设置JVM(Java虚拟机)的内存
    虚拟机内存的大小除了在web容器中设置,我们还可以通过系统环境变量来设置,下面看看设置步骤:1.打开windows系统环境变量,在系统变量中,新建变量JAVA_OPTS,值设置为-Xms1024M-Xm......
  • 初步理解:jvm运行机制,java程序运行机制,堆栈详解,jvm调优的目的。
    谷咕咕最近在准备面试,本来想多看看堆和栈的关系,看看发现又设计到gc(GarbageCollection)垃圾回收机制,发现盲区太多了,就去粗略的学习了一下jvm(java虚拟机),发现之前只会写程序,底......
  • JVM:本地方法栈
    java虚拟机栈用于管理java方法的调用本地方法栈用于管理本地方法的调用和java虚拟机栈一样,也是线程私有的也可动态扩增内存的大小当某个线程调用一个本地方法时,他就进入一......
  • JVM:运行时数据区-PC寄存器(程序计数器)
    JVM:运行时数据区1.什么是pc寄存器:JVM的pc寄存器也叫程序计数器,是对物理pc寄存器的一种抽象虚拟。用来存储指向一下条指令的地址,即将要执行的指令代码,由执行引擎读取下一......