Java虚拟机栈???????

Java虚拟机栈???????

 

“线程私有”内存空间;Java虚拟机栈空间是存放Java方法执行的线程内存模型;

 

 

我们需要分享Java虚拟机机栈空间里面的时候,需要贴上一张详细的图片来表示:

就是下图中所圈出的那块区域,结构很清晰,每个线程都会有一个栈空间,栈空间中存放了很多栈帧,那么这些栈帧中到底存放了些什么数据呢?

我们知道,在每个Java方法被执行的时候,Java虚拟机就会同时创建一个栈帧,那么多个方法,就会有多个栈帧;那么每个方法中都会对应到一个栈帧;然后我们思考一下,一个栈帧所对应的生命周期就是一个方法的执行开始与结束。所以我们可以拓展出,栈帧中存放是数据就是和当前方法所有关的数据,例如:方法中的局部变量表、操作数栈、动态链接,以及方法出口等信息。那么一个栈帧对应的生命周期就是该方法,那么当方法执行完成后,就需要在虚拟机栈中出栈,这就是一个执行过程。

 

 

这里,我们拓展一下思考:栈空间、和堆空间应该是我们最常听到的两个部分,甚至我们抛异常,也是需要打印所有栈异常信息,这样才能清晰的看出线程的执行过程,在哪里进入了哪个栈帧,在哪里出了哪个栈帧。

但我们从上图发现,栈是很大的一个概念,并不是一个空间,在上图中还有本地方法栈这个空间呢。

 

栈分析:

我们主要分析一下jvm虚拟机栈,其中的栈,我们有一定开发经验的同学都知道,一个web请求都是一个新的线程执行的,那么这个线程就会对应了一个java虚拟机栈的一个栈空间,这个空间是线程私有的。那么自然这个栈的生命周期就是和线程的生命周期一致。当线程创建,也就开辟的新的栈空间内存;当线程销毁,那么当前的栈空间也会被释放。

那么对于一个栈的数据结构,我们应当是有了解的,操作方式也会有“压栈”,“出栈”等方式,那么自然栈中就会有一系列的元素,称为“栈帧”,那么栈帧到底对应这我们具体的代码中的什么呢?其实就是对应着我们每个一个执行的方法。我们都知道,一个线程的请求,不可能是一个方法就直接返回了(很少,至少分层模型,不会直接一个方法做了所有的动作);那么就会涉及到多个方法的调用,那么这里就是压栈的动作,例如,线程先访问了a方法,那么就会把a方法对应的栈帧压栈入栈;这个时候,如果在a方法中又调用了b方法,那么就会把b方法对应的栈帧进行压栈操作,然后线程就会进入了b方法执行。当b方法执行完成,return的时候,那么我们就会把b方法对应的栈帧进行栈弹出,也就是出栈操作;当a方法执行完成后,return的时候,我们就会把a方法对应的栈帧进行栈弹出。这样就对应了栈的数据结构特性,先进后出规则。

对于一个栈结构,会有多个栈帧,那么当前执行的栈帧,也被称之为“当前栈帧”,按照我们知道执行方式,通常,顶部的栈帧就是当前正在执行的栈帧了。而每个栈帧都会对应了一个方法,那么这个方法就被称之为“当前方法”。每个方法都会属于一个类,这个类也同理,被称之为“当前类”.

栈空间都是线程私有的,也就不会出现数据安全问题了。栈帧之间是不会相互穿插使用的。

 

 

 

言归正传,我们还是继续来聊Java虚拟机栈中的每个线程的每个栈帧中具体存放了些什么数据。

其中主要存放了“局部变量表”、“操作数栈”、“动态链接”、“方法出口”,当然还有一些其他的数据,不止这4类了。

那么这四类大致都是些什么数据呢?

局部变量表:

上面也说道了局部变量表,就是对应的我们方法中的一些临时变量,例如,我们在方法中定义了一个fori循环,那么就会有一个循环的下标,那么这个下标就是一个临时变量,当方法执行完成后,这个变量也就释放了。注意,因为栈内存的原因,当我们出栈的时候,该变量就会被释放,都不用等到GC垃圾回收器来回收。

局部变量表中存放了很多Java的基本类型,8大基本类型(此处不写,大家应该都懂,不懂就别学这个了,先去看看Javase);然后还有一些对象引用的数据,也就是地址值引用的一些局部变量,例如在这个方法中定义了一个Integer minAge = MIN_AGE,将该对象引用了类常量数据;

然后局部变量表中也存放了返回地址信息“returenAddress”,这个信息,主要是为了给jsr、jsr_w和ret指令提供服务。

局部变量表我们可以抽象的理解为一个二维表结构数据槽,当在编译时期,该表结构就基本上可以确定了,因为当我们在分析字节码文件的时候,就可以确定出需要定义出哪些数据类型,同时也会申请一定的内存空间,作为线程栈空间使用,倘若内存不够使用了,那么就会出现两种情况,一种是支持动态扩展,也就是说,Java虚拟机支持如果栈内存不够,线程所申请的栈帧高度大于预期的申请,那么就可以动态扩容(同步方式申请),那么如果申请的虚拟机内存也不够了,那么就会出现OutOfMemoryError;如果说,Java虚拟机不允许扩展,那么就会直接出现StackOverflowError ,堆栈溢出问题,因为栈空间不足,单个栈帧超出预期。此处我们也可以学习到,千万不要在某个方法中频繁new大量的对象,使用大量的举报变量,例如,用一个大数组装大数据,且还是局部变量,这样会超出局部变量表中的栈帧的容量设定。(HotSpot虚拟机是不支持栈容量动态扩展了。)

其中我们都知道,长整型,双精度浮点类型都是占用64位长度的,那么所占用的局部变量表槽自然需要多占用一个位置了。这是个小知识点。

 

 

操作数栈:

操作数栈是个栈数据结构的结构,先进后出的特性。也是在编译是时候,被初始化好了的,也就是说,在一开始,编译器就确定了栈的最大深度,就直接初始化好了。

那么操作数栈中存放的是什么数据呢?

我们知道,在栈帧中,也就是一个对应的方法中,会存在很多局部变量的操作,那么也就会出现更多的临时变量,也随之会产生很多的变量执行顺序,那么随之字节码指令的执行,就会将局部变量表中,或者对象的实例中的属性所产生的变量或者常量,将其拷贝至操作数栈中,然后随之字节码指令的执行,也就会有入栈出栈的事件发生(记得有个面试题 i+1的问题,维护i值不变);那么操作数栈就是存放的是某个方法中执行的临时数据。

 

动态链接:

说到这个,首先得说一下后面概念中的“方法区”中的一个区域“运行时常量池”;“运行时常量池”不仅存放的是一些常量数据,还会存放“方法的符号引用”;怎么理解这个“方法的符号引用”呢?就是,我们通常a方法调用b方法,那么a方法调用b方法的方式称之为“符号引用”;那么为什么要存放到“运行时常量池”呢?这就是一个“内存地址的直接引用的过程了”,我们a方法调用b方法,从内存中进行加载b方法真实的对象的方法,而不再是绑死了一个地址。那么为什么存放在“运行时常量池”呢?我们知道,类的元数据,有方法等信息,方法通常来说,经过加载,基本地址不变了,那么就可以抽象的理解为是一种“常量数据”,那么放在“常量池”中也是可以的,且常量池的特性很符合存放此类数据。

那么科普完“内存地址的直接引用”;就该说说动态链接了。在Java虚拟机栈中,每个栈帧都会有一个“指向运行时常量池中的该栈帧所属方法的引用”,这个引用关系就是为了支撑动态代理。

听着就很迷茫,我们理解一下:Java有一大特性:多态。也就是说,我们假设 Pig pig = new ZhuBaJie();

这个应该知道,我们可以通过猪对象接收一个猪八戒的实例对象。但是在静态解析的时候,我们只能知道pig这个对象执行run()方法,是四只脚跑的。但是我们pig这个对象应该是猪八戒的跑的方式,是两只脚跑的,那么这个时候就不对了。我们在运行期间,所在栈帧中调用的pig.run()方法,应该是指向的是 ZhuBaJie类中的run()方法。

如果这样举例,是不是就很好理解动态连接的优势了,支持动态解析正确的方法。所以此命名“动态链接”为了解决多态问题中的真实的方法对象。(因为编译时期和运行时期可能不是同一个方法)

 

方法出口:

执行一个方法会有两种结果“成功返回”“失败返回”;

我们都碰到过方法中抛出异常,没有进行捕获处理,但是,他这个方法还是一层一层的方法如期返回出去了。除非我们进行了容错处理,提前知道此处会抛出异常,然后并对异常进行了处理,方法才会正常的执行下去,然后成功返回。对于异常处理,每个方法中都会有个异常表,用于存放,每段代码块中所被拦截的异常。

对于正常返回,那么就是遇到了return关键字,或者执行完整方法体

两种返回方式最终的返回位置都会回到该方法所被调用的位置。

正常返回的时候,返回的位置,就是需要程序计数器进行提供数据,可能是一个方法的某个位置,可能是下条指令的地址值。

异常返回的时候,那就得看我们有没有对其进行异常拦截了,这个依赖于异常处理表。如果没有对应的异常处理表处理,那么将会返回到该方法所被调用的位置,但是不会返回任何数据。

 

附加信息:

这个就是比较不确定了,可有可无的空间数据。既然提到了,那非得有,那可能就是一些调试相关的数据,在不同的虚拟机实现中,会有不同的结论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值