JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
JVM结构
JVM(Java虚拟机)
|--类装载子系统
|--运行时数据区(内存模型)
| |--堆
| |--栈(线程)
| |--本地方法栈
| |--方法区(元空间)
| |--程序计数器
|--字节码执行引擎
栈(Stack)
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部表量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直到执行结束,就对应着一个栈帧从虚拟机中入栈到出栈的过程。
==当一个线程启动时,JVM 会从栈内存分配专属的内存区域,用于存放线程的局部变量。==其特点为先进后出(FILO)。
栈帧(Stack Frame)
JVM 执行 Java 程序时需要装载各种数据到内存中,不同的数据存放在不同的内存区中(逻辑上),这些数据内存区称作运行时数据区(Run-Time Data Areas)。
其中 JVM Stack(Stack 或虚拟机栈、线程栈、栈)中存放的就是 Stack Frame(Frame 或栈帧、方法栈)。
当线程启动时每个方法都会有局部变量,JVM 会在当前栈内存中分配新的空间去存储每个方法的局部变量,这些被分配的独立空间叫做栈帧。即一个方法对应一块栈帧内存区域。
栈帧结构
每个方法栈帧内部存储包括:局部变量表,操作数栈,动态链接,方法出口等主要内容。
局部变量表
java局部变量表是栈帧重要组中部分之一。他主要保存函数的参数以及局部的变量信息。局部变量表中的变量作用域是当前调用的函数。函数调用结束后,随着函数栈帧的销毁。局部变量表也会随之销毁,释放空间。
当局部变量表中的变量为对象类型时,对象一般会存储在堆中,局部变量表则存储堆中对象的地址(指针)。
操作数栈
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)。操作数栈和局部变量表在访问方式上存在着较大差异,操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,一个32bit的数值可以用一个单位的栈深度来存储,而2个单位的栈深度则可以保存一个64bit的数值,当然操作数栈所需的容量大小在编译期就可以被完全确定下来,并保存在方法的Code属性中。
方法出口(方法返回地址)
记录方法结束时,应回到调用方法的方法中哪个代码位置。本质上方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码执行引擎工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器是每一个线程独有的,其记录线程运行代码的位置(简单理解为行号)。
字节码指令示例:
public void testAddOperation();
Code:
0: bipush 15
2: istore_1
3: bipush 8
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
在线程被恢复,程序的分支、循环、跳转、异常时、都需要程序计数器对字节码执行进行记录。
通过字节码执行引擎修改程序计数器的记录。
方法区(元空间)
线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量,静态代码块、即时编译器(JIT Compiler)编译后的代码数据等,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
字节码文件通过类装载子系统进行解析进入方法区,当静态变量为对象类型时,对象会存储在堆中,方法区中存储的是堆中对象的地址(指针)。
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native(本地)方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
堆(Heap)
堆在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的,这是一块与java应用密切相关的内存空间。堆是Java垃圾收集器管理的主要区域(GC堆),垃圾收集器实现了对象的自动销毁。堆可以细分为:新生代和老年代;再细致一点的有Eden空间,From Survivor空间,To Survivor空间等。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。可以通过-Xmx和-Xms控制。
创建新对象实例时, 一般存入到Eden区。当Eden区存满时,JVM 会调用 minor gc 对Eden区中的垃圾对象进行清除(将GC Roots对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象, 其余的都是垃圾对象),将存活的对象进行复制到Survivor(From)区中,并且对象头信息中的分代年龄进行+1操作。当Eden再次存满时,minor gc会将Eden和*Survivor(From)中存活的对象进行复制并存入Survivor(To)*中,并将其它两区进行清除。依次类推,当存活的对象分代年龄大于15时(可设置大小),将这些对象存入老年代中,不再对老年代中的对象进行清理。直到老年代中存满时,JVM 会调用一次==full gc对整个堆进行清理,清理时会进行STW== (Stop The Work -暂停应用所有线程)操作,如果老年代还是存满状态则会==OOM== (Out Of Memory - java.lang.OutOfMemoryError)异常。
GC Roots模型:
扩展:
-
JVM调优目的:减少JVM 调用==full gc(运行时间长)的次数,从而减少STW发生。虽然minor gc==也会触发STW的发生,但其执行时间短,用户无感知。
-
对象进入老年代几种方式:
-
长期存活的对象将进入老年代,每执行一次minor gc,存活的对象年龄加1,当年龄到达15(默认
-XX: MaxTenuringThreshold
指定)时进入老年代。 -
对象动态年龄判断:一批对象的总大小大于将存入的Survivor区内存的
50%
时(-XX: TargetSuvivorRatio
指定)
-