jvm-运行时数据区

线程

  • 线程是一个程序里的运行单元,JVM允许一个程序有多个线程并行的执行;
  • 在HotSpot JVM,每个线程都与操作系统的本地线程直接映射。
  • 当一个java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。java线程执行终止后。本地线程也会回收。
  • 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用java线程中的run()方法.
  • 查看jvm线程的方法:使用jconsole

HotSpot JVM后台主要线程:

  • 虚拟机线程:这种线程的操作时需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行包括“stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销
  • 周期任务线程:这种线程是时间周期事件的提现(比如中断),他们一般用于周期性操作的调度执行。
  • GC线程:这种线程对于JVM里不同种类的垃圾收集行为提供了支持
  • 编译线程:这种线程在运行时会将字节码编译成本地代码
  • 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。

程序计数器(pc寄存器)

JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装到寄存器才能够运行。JVM中的PC寄存器是对计算机PC寄存器的一种抽象模拟。

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

  • 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
  • 在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined)。
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  • 字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令
  • 它是唯一一个在java虚拟机规范中没有规定任何OOM情况的区域

avatar

本地方法栈

  • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用
  • 本地方法栈,也是线程私有的。
  • 允许被实现成固定或者是可动态拓展的内存大小。(在内存溢出方面是相同的)
  • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError异常。
  • 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么java虚拟机将会抛出一个OutOfMemoryError异常。
  • 本地方法是使用C语言实现的。它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库
  • 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限
  1. 本地方法可以通过本地方法接口来 访问虚拟机内部的运行时数据区
  2. 它甚至可以直接使用本地处理器中的寄存器
  3. 直接从本地内存的堆中分配任意数量的内存
  • 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
  • 在hotSpot JVM中,直接将本地方法栈和虚拟机栈合二为一。

虚拟机栈

虚拟机栈背景

由于跨平台性的设计,java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。

优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

内存中的堆与栈

  • 栈是运行时的单位,而堆是存储的单位;即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
  • 一般来讲,对象主要都是放在堆空间的,是运行时数据区比较大的一块
  • 栈空间存放 基本数据类型的局部变量,以及引用数据类型的对象的引用
  • java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。
  • 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应这个一次次的java方法调用。它是线程私有的
  • 生命周期和线程是一致的
  • 作用:主管java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
  • 局部变量:相对于成员变量(或属性)
  • 基本数据变量: 相对于引用类型变量(类,数组,接口)

栈的特点

  • 栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器(程序计数器)
  • JVM直接对java栈的操作只有两个
  • 每个方法执行,伴随着进栈(入栈,压栈)
  • 执行结束后的出栈工作
  • 对于栈来说不存在垃圾回收问题

栈中可能出现的异常

java虚拟机规范允许Java栈的大小是动态的或者是固定不变的

  • 如果采用固定大小的Java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个 StackOverFlowError异常
  • 如果java虚拟机栈可以动态拓展,并且在尝试拓展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个 OutOfMemoryError异常。

设置栈的内存大小

我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。 (IDEA设置方法:Run-EditConfigurations-VM options 填入指定栈的大小-Xss256k)

栈的存储结构和运行原理

  • 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
  • 在这个线程上正在执行的每个方法都对应各自的一个栈帧
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息;包括局部变量表,操作数栈,动态链接,方法返回地址
  • JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出/后进先出的和原则。
  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Frame)
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
  • 不同线程中所包含的栈帧是不允许相互引用的,即不可能在另一个栈帧中引用另外一个线程的栈帧
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
  • Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

avatar

局部变量表

  • 局部变量表被称之为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用,以及returnAddress类型;
  • 局部变量表所需的大小事在编译期间确定下来的。
  • 局部变量表,最基本的存储单元是Slot(变量槽)
  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用越多。
  • 局部变量表中的变量只在当前方法调用中有效,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

变量对比

  • 类变量:linking阶段的prepare阶段:给类变量赋默认值,initial阶段,给类变量显示赋值
  • 实例变量:随着对象的创建,会在堆中分配实例空间变量,并进行默认赋值
  • 局部变量:使用前必须显示赋值,否则编译不通过

Heap,通过new关键字创建的对象,都存放在堆内存中。

  • 特点:
  1. 线程共享,堆中的对象都存在线程安全的问题
  2. 垃圾回收,垃圾回收机制重点区域。
  • jvm内存的划分:
  1. JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
  2. 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
    非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
  3. 年轻代(New):年轻代用来存放JVM刚分配的Java对象。
  4. 年老代(Tenured):年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代。
  5. 永久代(Perm):永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,一般设置为128M就足够,设置原则是预留30%的空间,方法区。