JVM-运行时数据区

148 阅读14分钟

运行时数据区结构

线程共享区

  • 方法区

线程私有区

  • 虚拟机栈
  • 本地方法栈
  • 程序计数器

程序计数器(PC Register)

介绍

  • 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的内存区域。
  • 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
  • 任何时间一个线程都只有一个方法在执行,也即是所谓的当前方法。程序计数器会存储但前线程正在执行的Java方法的JVM的指令地址:或者,如果是在执行native方法。则是未指定值。
  • 它是唯一一个在Java中没有OOM的区域

作用

PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。

问题

1. 使用PC寄存器存储字节码指令地址有什么用呢?

因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

2.PC寄存器为什么会被设定为线程私有?

为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的方法自然就是为每一个线程都分配一个PC寄存器。

虚拟机栈

基本内容

  • 每个线程创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的JAVA方法调用,是线程私有的。
  • 生命周期和线程一致。
  • 主管Java程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。
  • 对于栈来说不存在垃圾回收问题
  • 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,Java虚拟机将会抛出StackOverFlow异常。
  • 如果Java虚拟机可以动态扩展,并且在尝试拓展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的空间区创建新的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemory异常。

栈的运行原理

  • 在一条活动线程中,一个时间点上,只有一个活动的栈帧。即只有当前正在执行的方法的栈帧是有效的,这个栈帧就被称为当前栈帧,与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • PC寄存器也是针对当前栈帧进行操作。

栈帧的内部结构

1.局部变量表

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。
  • 是线程的私有数据,不存在数据安全问题。
  • 局部变量表所需的容量大小是在编译期确定下来的。在方法运行期间是不会改变大小的。
  • 当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

-Slot

  • 局部变量表最基本的储存单位是Slot变量槽
  • 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型的变量
  • 在局部变量表里,32位以内的类型只占用一个slot,64位的类型占用两个slot
  • JVM会为局部变量表中的每一个slot都分配一个索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
  • 普通方法与静态方法不同的一点是,在普通方法中可以使用this,因为在普通方法中的局部变量表里的index0储存的正是this,所以可以使用,而静态方法中的局部变量表没有。
  • 栈帧中的局部变量表中的变量槽是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量就很有可能会复用过期局部变量的槽位,从而节省资源。

-静态变量与局部变量

  • 按照数据类型分为:基本数据类型,引用数据类型
  • 按照类中申明的位置:成员变量,局部变量
  • 成员变量:分为类变量实例变量,类变量在链接的准备阶段赋值零值在初始化阶段显式赋值,实例变量是随着对象的创建,会在堆空间中分配实例变量空间,并经行默认赋值。在使用前,都经历过默认初始化赋值。
  • 局部变量:在使用前,必须进行显式赋值,否则编译不通过。

2.操作数栈

  • 在方法的执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈。
  • 操作数栈在编译期其最大深度就已经定义
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

-栈顶缓存技术

将栈顶元素全部缓存在物理cpu寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率

3.动态链接

  • 每一个栈帧内部都包含一个指向常量池中该栈帧所属方法的引用。
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在Class文件的常量池里。动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

4.方法返回地址

  • 存放调用该方法的pc寄存器的值,(PC寄存器存储的是该方法要执行的下一条指令的值)
  • 一个方法的结束,有两种方式:1.正常执行完成。2.出现未处理的异常,非正常退出。

5.一些附加信息

本地方法栈

本地方法

  • 一个Native Method就是一个Java调用非Java代码的接口。
  • 标识符native可以与所有其他的java标识符连用,但是abstract除外
  • 有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。
  • 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。本地方法哭呀通过本地方法接口来访问虚拟机内部的运行时数据区。

核心概述

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。
  • 堆可以处于物理上不连续的内存空间,但在逻辑上它应该是被视为连续的
  • 所有线程共享的Java堆,在这里还可以划分线程私有的缓冲区(TLAB)
  • 方法结束后,堆中的对象并不会立即被回收,仅仅在垃圾收集时才会被移除。

内存细分

  • JDK7及以前逻辑上分为三部分:新生代+老年代+永久代
  • JDK8及以后逻辑上分为三部分:新生代+老年代+元空间

堆空间大小设置及参数设置

  • -Xms:用来设置堆空间(新生代+老年代)的初始内存大小,-X是jvm运行参数,ms是memory start,默认为电脑内存大小/64
  • -Xmx:用来设置堆空间(新生代+老年代)的最大内存大小,默认为电脑内存大小/4
  • -XX:NewRatio=2 老年代/新生代比例,默认为2
  • -XX:SurviveRatio:配置新生代中Eden区和s区的空间比例
  • -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
  • 建议把初始内存和最大内存设置成一样的值
  • -Xmn:设置新生代的大小
  • -XX:+PrintFlagsInitial:查看所有的参数的默认初始值
  • -XX:+PrintFlagsFinal:查看所有的参数的最终值

新生代(Eden空间,Survive0空间,Survice1空间)

  • 在HotSpot中,Eden:S0:S1=8:1:1
  • 几乎所有对象都是在Eden区new出来,绝大多数在Eden销毁。

对象分配的过程

  • new的对象先放Eden区,此区有大小限制
  • 当Eden区的空间满时,程序又需要创建对象,JVM的垃圾回收器将对Eden进行垃圾回收(Minor GC),将Eden区中和S区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到Eden区。
  • 然后将Eden区中的剩余对象移动到S0区
  • 如果再次触发Minor GC,此时上次幸存下来放到S0的,如果还没有被回收,就会放到S1区。
  • 如果再此Minor GC就会按照上述规则在S0与S1之间循环
  • 循环超过阈值后,可以到老年代。
  • 只有Eden区满时才会触发Minor GC,S1,S0满时不会触发。

特殊情况

  1. new出来的对象大小大于Eden区时,会去判断老年代是否放得下,如果放得下,则将其直接存放在老年代中,如果存放不下,会对老年代进行一次Major GC,在进行判断,如果还放不下,则OOM。
  2. Minor GC后如果S区是满的,且有Eden的对象要放到S区,那么这个对象将直接放到老年代。

Minor GC ,Major GC,Full GC

针对HotSpot VM的实现,它里面的GC按照回收区域分为:部分收集整堆收集

部分收集:不是对整个Java堆的垃圾收集

  • 新生代收集(Minor GC):对Eden区的垃圾收集,当Eden区满时会触发,S区满时不会触发
  • 老年代收集(Major GC):对老年代的垃圾收集,目前只有CMS GC会单独收集老年代的行为。在老年代空间不足,且minor GC后仍不足会触发Major GC,Major GC的时间更长,STW时间更长。GC后内存如果仍不足,报OOM。
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集

整堆收集

  • (Full GC):收集整个Java堆和方法去的垃圾收集

TLAB

  • JVM为每一个线程分配了一个私有缓冲区域,它包含在Eden空间内。
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,
  • 同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略。
  • 可以通过参数 -XX:+useTLAB 进行开启,默认是开启的。

分配担保策略

  • 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
  • 如果大于,则此此Minor GC是安全的。
  • 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
  • 如果HandlePromotionFailure=true,则表示允许担保失败,那么将会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
  • 如果大于,则尝试一次有风险的Minor GC。
  • 如果小于,则进行一次Full GC。
  • 如果HandlePromotionFailure=false,则改为进行一次Full GC。
  • JDK7后规则变为只要老年代的连续空间大于新生代对象大小或者历次晋升的平均大小就会进行Minor GC,否则进行FUll GC。

方法区

栈,堆,方法区的交互关系

qq_pic_merged_1658384786591.jpg

方法区的理解

  • 方法区与Java堆一样,是各个线程共享的内存区域。
  • 方法区在JVM启动的时候被创建,并且它实际的物理内存空间可以是不连续的。
  • 方法区的大小是可以分配的,如果系统定义了太多的类,方法区会抛出OOM。
  • 关闭JVM就会释放方法区空间。
  • 对于HotSpot,在JDK7及以前,方法区也称为永久代,在JDK8及后,使用元空间代替了永久代。

方法区大小参数设置

  • -XX:MetaspaceSize=100m(方法区初始值大小)(默认为21M)
  • -XX:MaxMetaspaceSize=100m(方法区最大值大小)(默认为-1,无限制)

方法区的内部结构

类型信息

对于每一个加载的类型,JVM必须在方法区中储存以下类型信息

  • 这个类型的个完整有效名称(全名=包名。类名)
  • 这个类型直接父类的完整有效名(对于interface或者Object都没有父类)
  • 这个类型的修饰符
  • 这个类型直接接口的一个有序列表

域信息

  • JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
  • 域的相关信息包括:域名称,域类型,域修饰符

方法信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序。

  • 方法名称
  • 方法的返回类型
  • 方法参数的数量和类型(按顺序)
  • 方法的修饰符
  • 方法的字节码,操作数栈,局部变量表及大小(abstract和native方法除外)
  • 异常表

运行时常量池,常量池

  • 常量池是class文件的一部分,其内储存的数据包括:数量值,字符串值,类引用,字段引用,方法引用,它就相当于一张表,JVM指令可以通过这张表找到要执行的类名,方法名,参数类型,字面量的类型。这部分内容在类加载后将放在方法区的运行时常量池中。JVM为每一个已加载类型都维护一个常量池。池中的数据是通过索引进行访问。

  • 运行时常量池是方法区的一部分,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。包含多种不同的常量,包括编译器就已经确定的数值字面量,也包括到运行期解析后才能获得的方法或者字段引用,此时不再是常量池中的符号地址了,而是转为真实地址。它具有的一个重要特性是:具备动态性

垃圾回收

方法区的垃圾收集主要回收两部分内容,常量池中废弃的常量和不再使用的类型。 判断一个类型不再被使用需要同时满足以下三种情况。

  • 该类所有的实例已经被回收,也就是Java堆中不存在该类及其任何派生此类的实例。
  • 加载该类的那加载器已经被回收这个条件,除非是经过精心设计的,可替换类加载器的场景,否则通常是很难达成的。
  • 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方,通过反射访问该类的方法。