没人看的JVM之方法的运行与虚拟机栈(2)

JVM内存区域

运行时数据区(必须掌握)

在这里插入图片描述
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域 Java 引以为豪的就是它的自动内存管理机制。相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。

  1. 直接内存:前面我们说了JVM内存是虚拟化的,那部分没有被虚拟的内存就叫直接内存也可以叫做堆外内存。这个虽然不是运行时数据区的一部分,但是会被频繁使用。你可以理解成没有被虚拟机化的操作系统上的其他内存(比如操作 系统上有 8G 内存,被 JVM 虚拟化了 3G,那么还剩余 5G, JVM 是借助一些工具使用这 5G 内存的,这个内存部分称之为直接内存)
  2. 运行时数据区:JVM内存虚拟化的那部分内存就是运行时数据区
  3. 运行时数据库有5大模块,按照理解我把他分成堆区,方法区,栈区,其中栈区包括虚拟机栈,本地方法栈,程序计数器
  4. 其中方法区和堆是线程共享的,栈是线程私有的,栈的生命周期和线程生命周期一致。

栈区在这里插入图片描述

虚拟机栈是基于线程的:哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。
虚拟机栈的大小初始大小为1M,可用参数 –Xss 调整大小,例如-Xss256k。
不同机器的初始大小其实也是不同的在这里插入图片描述
上面的资料来自官方文档(JDK1.8):https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

栈帧

栈帧:在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。
我们来看一段代码
在这里插入图片描述
这段代码很简单,就是起一个 main 方法,在 main 方法运行中调用 A 方法,A 方法中调用 B 方法,B 方法中运行 C 方法。 我们把代码跑起来,线程 1 来运行这段代码, 线程 1 跑起来,就会有一个对应 的虚拟机栈,同时在执行每个方法的时候都会打包成一个栈帧。 比如 main 开始运行,打包一个栈帧送入到虚拟机栈。如下图所示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

根据栈的特点: C 方法运行完了,C 方法出栈,接着 B 方法运行完了,B 方法出栈、接着 A 方法运行完了,A 方法出栈,最后 main 方法运行完了,main 方法这个栈帧就 出栈了。 这个就是 Java 方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的。
从上面可以看出来每一个方法就是一个栈帧
栈帧大体都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)
局部变量表顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个 32 位的长度,主要存放我们的 Java 的八大基础数据 类型,一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的 Object 对象,我们只
需要存放它的一个引用地址即可。
返回地址正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定) 注:如何调用程序计数器中的地址来返回在后面详细说。
动态连接:这个后面在说。
操作数据栈(非常重要):存放 java 方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的 java 数据类型,所 以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。 操作数栈本质上是 JVM 执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。
注意:同时,虚拟机栈这个内存也不是无限大,它有大小限制,默认情况下是 1M。 如果我们不断的往虚拟机栈中入栈帧,但是就是不出栈的话,那么这个虚拟机栈就会爆掉。会出现这个异常java.lang.StackOverflowError
每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分 配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

程序计数器

为什么会有程序计数器:因为电脑1S中可以完成的工作时非常巨大的,如果先完成一个任务在去完成下一个任务那么其他任务就会卡住,所以线程都是通过获取cpu时间片来工作的,他会左右切换来完成几个任务,因为速度很快所以看起来是完成的一样。
程序计数器的特点和作用较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。 程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址。 由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其
它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。

注意:因为 JVM 是虚拟机,内部有完整的指令与执行的一套流程,所以在运行 Java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如 果是遇到本地方法(native 方法),这个方法不是 JVM 来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器, 这个会记录本地代码的执行的地址,所以在执行 native 方法时,JVM 中程序计数器的值为空(Undefined)。 另外程序计数器也是 JVM 中唯一不会 OOM(OutOfMemory)的内存区域。

虚拟机栈的具体操作流程

在这里插入图片描述
我们来看一段代码,然后反编译获取的他的字节码文件在这里插入图片描述
在这里插入图片描述
上面就是code代码了 为了让大家可以看懂,献上官网的链接和截图
官网链接https://cloud.tencent.com/developer/article/1333540
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
我们来分析一下这段code代码干了些什么事情

0:iconst_1 将一个常量压入操作数栈,横杠后面的数字代表值。
在这里插入图片描述
1:istore_1将操作数栈的值放入局部变量表,横杠后面的是局部变量表的位置,就是说把刚刚压入操作数栈的值取出来放到1这个位置。
在这里插入图片描述
执行完第三行以后变成
在这里插入图片描述
这个过程在jvm对应代码是
在这里插入图片描述
在这里插入图片描述
这一大段对应的则是
在这里插入图片描述
来一步步的分析
4和5就是说把局部变量位置1和位置的值加载到操作数栈在这里插入图片描述
注意2是一定在1上面的因为栈是先进后出的
6:iadd这一步 其实有三个步骤 首先取出操作数栈的值,然后 执行相加操作,然后重新放入操作数栈
在这里插入图片描述
7:bipush则是放入一个10
9:imul和6是一样的也是3部。执行结果如下
在这里插入图片描述
可能有人注意到了少了一个8,8 是cpu层面的工作 先不用管
10:istore_3 将值保存到局部变量表的3这个位置
在这里插入图片描述
在这里插入图片描述
代码里面的这个return z;
对应最后两步首先将3位置的值,重新加载到操作数栈,然后弹出当前栈帧。

完成出口(返回地址)
正常返回:(调用程序计数器中的地址作为返回)
三步曲: 恢复上层方法的局部变量表和操作数栈、 把返回值(如果有的话)压入调用者栈帧的操作数栈中、 调整程序计数器的值以指向方法调用指令后面的一条指令、
异常的话:(通过异常处理表<非栈帧中的>来确定)

本地方法栈

本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java
函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是 用 Java 实现的,而是由 C 语言实现的(比如
Object.hashcode 方法)。 本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native
方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。

虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot 直接把本地方法栈和虚拟机栈合二为一 。

方法区

方法区(Method Area)是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime
Constant Pool) 字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法 方法区是
JVM 对内存的“逻辑划分”,在 JDK1.7 及之前很多开发者都习惯将方法区称为“永久代”,是因为在 HotSpot
虚拟机中,设计人员使用了永 久代来实现了 JVM 规范的方法区。在 JDK1.8 及以后使用了元空间来实现方法区

元空间

方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入
JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。 在 HotSpot 虚拟机、Java7
版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本
已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地内存。
元空间大小参数: jdk1.7 及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize; jdk1.8
以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize jdk1.8

做一个思考题Java8 为什么使用元空间替代永久代,这样做有什么好处呢?

答案:移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。 永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为
8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有为 PermGen 分配多大的空间很难 确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等

运行时常量池

运行时常量池(Runtime Constant
Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编
译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。 运行时常量池是方法区的一部分。运行时常量池相对于 Class
常量池的另外一个重要特征是具备动态性(Class 常量池在类加载章节会具体讲)。

堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。
堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的。
随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage
Collection)。 那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java
类中存在的位置。 Java 的对象可以分为基本数据类型和普通对象。 对于普通对象来说,JVM
会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。
对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。
当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配

常用的设置参数

-Xms:堆的最小值; -Xmx:堆的最大值; -Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值; 例如- Xmx256m
在这里插入图片描述

直接内存(堆外内存)

直接内存有一种更加科学的叫法,堆外内存。 JVM
在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的
内存也就是堆外内存。 它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域;如果使用了
NIO,这块区域会被频繁使用,在 java 堆内可以用 directByteBuffer 对象直接引用并操作; 这块内存不受 java
堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现
OOM 异 常。小结: 1、直接内存主要是通过 DirectByteBuffer
申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小。 2、其他堆外内存,主要是指使用了 Unsafe 或者其他
JNI 手段直接直接申请的内存。 堆外内存的泄漏是非常严重的,它的排查难度高、影响大,甚至会造成主机的死亡。后续章节会详细讲。 同时,要注意
Oracle 之前计划在 Java 9 中去掉 sun.misc.Unsafe API。这里删除 sun.misc.Unsafe
的原因之一是使 Java 更加安全,并且有替代方案。 目前我们主要针对的 JDK1.8,JDK1.9 暂时不放入讨论范围中,我们大致知道
java 的发展即可。

那么内存结构就到这里结束了。如果文章中有错误或者不准确的地方,还望大家指正。有想共同学习的也可以加我微信,一起共同交流。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值