详细分析JVM运行时数据区

运行时数据区

Java程序在运行期间,会产生各种类型的数据,而这些运行时的数据就会存储到JVM中的运行时数据区。根据JVM规范,将内存共分为 程序计数器,虚拟机栈,本地方法栈,堆,方法区 五个部分

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9jYEKUCh-1596130894248)(/Users/guo/Library/Application Support/typora-user-images/image-20200729114138571.png)]

程序计数器

程序计数器也叫做PC寄存器,它存储了指令相关的信息。它是内存中很小的一块区域,与其他数据区相比是运行速度最快的区域。主要的作用就是存储指向下一条要执行指令的地址,执行引擎会获取到下一条指令,然后执行。

由于Java是支持多线程执行的,但是CPU同一时刻只能执行一个线程,CPU就是通过不停的切换时间片来控制多个线程的执行,而程序计数器就是用来保证线程在拿到时间片后回到之前执行到的地方继续执行

程序计数器的特点

  • 线程私有,每个线程都有自己独立的程序计数器,生命周期与线程的生命周期一致
  • 如果执行的是Java方法,会保存虚拟机正在执行的字节码文件指令地址,如果执行的是Native方法,程序计时器则为空。
  • 是虚拟机运行时数据区中唯一不会发生OutOfMemoryError的内存区域

虚拟机栈

虚拟机栈是用来描述Java方法执行时的内存模型,每执行一个方法就会在虚拟机栈中生成一个栈帧,如同程序计数器一样也是线程私有的,每个线程在创建时都会创建一个栈帧。

虚拟机栈的特点

  • 虚拟机栈是基于栈的数据结构,有入栈和出栈两种操作
  • 虚拟机栈不会被GC

栈属于一种数据结构,在虚拟机栈中他的内存大小有两种模式,一是动态模式,另一种就是固定大小模型;

动态模式:当虚拟机栈采用动态模式进行深度设置时,如果方法在执行时没有申请到足够的内存大小,或在创建新的线程时没有足够的大小去创建新的栈帧,就会抛出OutOfMemoryError异常

固定大小:当虚拟机采用固定大小模式设置栈的深度,如果线程请求分配的内存空间超过了栈设置的最大容量时,会抛出StackOverflowError异常。固定大小可以通过 -Xss 参数来设置

栈帧

栈帧是虚拟机栈中的基本存储单位,每个栈帧都对应了线程上执行的方法,它保存了方法在执行过程中的各种数据信息。

栈帧实际上是一个数据集,它包含了本地变量表,操作数栈,动态链接,方法返回地址等。在虚拟机栈中,一个方法的运行对应着向虚拟机栈中压入一个栈帧,方法的返回就对应着栈帧的出栈。

本地变量表:定义为一个数组,用来存储方法的入参及定义在方法内部的局部变量,包括基本数据类型,对象的引用及返回地址类型。一个局部变量可以保存boolean、byte、float、short、char、int和reference、returnAddress数据。long、double类型的数据会占用两个局部变量

本地变量表在编译器内就确定了大小并完成了内存分配,在程序运行期间局部变量表大小不会改变。

局部变量表中的基本存储单元为Slot,可以理解为上面所说的局部变量,对于一个32位的数据类型,占用一个Slot,而对于64位的数据类型,占用两个Slot。

在局部变量表中,虚拟机是通过索引定位的方式来访问局部变量表中的变量,其索引由0开始到局部变量表最大Slot数量。在索引访问的时候,如果是32位的数据,其索引就是n,对于64位的数据,其索引位置为n,n+1两个Slot。如果当前栈帧是通过构造方法或者实例方法创建的,那么在局部变量表index位0的位置,会保存改对象的引用this。

栈帧中的Slot是可以重复利用的,局部变量表中的变量也是垃圾回回收中的GC ROOT 节点,只要是局部变量表中直接或者间接引用的对象都不会被回收。

操作数栈:主要用于保存计算过程中的中间结果,同时作为计算过程中临时变量的存储空间,可以将操作数栈看作成一个方法执行的真正的工作区。也是一个先进先出的栈结构。在执行方法的过程中,根据字节码指令往操作数栈中写入或提取数据。在新的栈帧被创建的时候,其操作数栈是空的。任何一个操作数栈都会有一个明确的栈深度用于存储方法执行过程中的数据。与局部变量表一样,操作数栈的大小在编译的时候就确定了大小。

操作数栈中的元素可以是任意的Java数据类型,同局部变量表一样,64位的数据类型占用两个栈单位。但是其访问方式并不是像局部变量表一样通过索引访问的。操作数栈的访问是通过标准的入栈和出栈来进行数据访问的。

动态链接:也可以称作为指向运行时常量池的方法引用。在虚拟机栈的栈帧中,每一个栈帧都包含了指向运行时常量池中该栈帧所属方法的引用,也就是存储的指向运行时常量池中方法的引用,通过这个引用去找到调用的方法。

方法返回地址:在A方法中调用了B方法,当B方法执行结束正常返回后会回到A方法内,返回地址就是记录了B方法结束后回到A方法的那一行继续执行。

本地方法栈

Java程序在运行中,可能会调用一些底层Native方法,这些方法的实际上与Java方法一样,只不过是底层C++语言实现的,方法的执行同样需要内存空间来存储执行过程中产生的一些数据,而本地方法栈就是用来存储这些Native方法的调用。本地方法栈与虚拟机栈结构类似。

方法区

方法区主要存储的是被虚拟机加载的类信息,包括了类的名字,方法信息,字段信息,常量、静态变量、即时编译器编译后的代码 等数据。

在JDK1.7前通常将方法区称为永久代,从1.8开始使用元空间取代了永久代,并在本地内存中实现的元空间;

元空间本质上与永久代相同,都是对JVM规范中的方法区的一种实现,不过两者的区别在于元空间不

方法区和堆一样可以不需要连续的内存,可以选择固定大小或动态拓展,还能选择是否实现垃圾收集。GC相对来说在方法区较少出现。方法区的GC主要是针对常量池的回收和类型的卸载。当方法区无法满足内存分配需求时便会抛出OOM。

运行时常量池属于方法区的一部分,通常存储的是编译期所产生的的字面量及符号引用,当类被加载后,这些数据会由class常量池保存到运行时常量池中;

前面类加载的文章中提到了类加载的详细过程,当类在完成加载后就是存储在方法区中

方法区的特点:

  • 方法区是线程共享的
  • 在JVM启动时创建,实际的物理内存与堆一样都可以是不连续的

在java虚拟机中的运行时数据区中,堆是内存空间最大的部分,堆同样伴随着虚拟机的创建而创建,对于对象的实例都是存储在堆内存中。GC的主要回收区域也是堆空间。

通过垃圾回收的角度来看,现在的垃圾收集器基本都采用的是分代收集算法,所以堆空间又分为新生代和老年代。

新生代与老年代比例是1:2,在新生代中还划分出三个区域eden区,Surivivor From区、Surivivor To区,三者的比例是8:1:1;

由于堆内存是线程共享的,所以难免有可能会出现多线程在堆中的同一块内存区域创建对象,而如果每次创建对象都去加锁,那实在是太影响效率了,所以HotSpot虚拟机为了解决这个问题,给每个线程在eden中分配了一小块的私有空间用来创建对象,这个部分称为**TLAB(本地线程缓冲区)**区域,通过这种方式解决了多线程在并发创建对象时的内存问题;默认会占用edem百分之一的空间,但是如果创建了一些大对象,这个私有空间放不下就会直接放在eden区中或老年代中。

新创建的对象会被分配在eden区中,当eden区存满了后会触发Monir GC,根据可达性算法标记出存活的对象后将存活的对象复制到S0区,将对象头中的分带年龄+1,并将eden区中的对象全部清除。然后后续创建的对象继续存放在eden中,当eden区再次存满后触发的Monir GC会同时清理eden区和S0区中的垃圾对象,然后将存活对象标记出来并复制到S1区(同时增加对象的分带年龄),并清除eden和S0区,如此循环直至对象分带年龄到达15后会被转移到老年代中。

进入老年代的条件:

  • 对象的分代年龄达到了15(这个年龄可以通过参数 -XX:MaxTenuringThreshold 来控制)
  • 新创建出的大对象可能会直接放入老年代 (超过了设置的 -XX:PretenureSizeThreshold 参数指定大小的对象)
  • 在Survivor区中,如果一批对象的大小大于这块Survivor内存的百分之50那么大于这批对象年龄的对象就会直接进入到老年代
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值