虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域各有各自的用途,以及创建和销毁时间,有的区域随着虚拟机在进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
一、程序计数器(重点)
程序计数器(Program Counter Register) 是一块较小的内存,你可以看作是当前线程所执行字节码的行号指示器
字节码解释器工作时就是通过改变这个计数器的值来获取吓一步需要执行的字节码指令,他是程序控制流的指示器,分支、跳转、循环、异常处理恢复等基础功能都需要依赖程序计数器来完成。
CPU是多线程切换的执行机制,为了保证线程切换后可以恢复到正确的位置,每个线程的都有独立的线程计数器,每个线程的计数器互不影响,独立储存。我们称这类内存是线程私有内存
二、虚拟机栈 —— JVM stack(重点)
1.程序基于栈的执行过程
java虚拟机栈 —— 线程私有,生命周期与线程相同
虚拟机栈描述的是方法执行的线程内存模型,每个方法被执行的时候都会创建一个栈帧,用户存储局部变量表、操作数栈、动态链接、方法出口等信息。JVM 中 每个线程对应一个虚拟机栈,每个方法对应一个栈帧。
方法从调用到执行完毕的过程 就对应着一个栈帧在虚拟机栈中 由入栈到出栈的过程。
程序的运行其实就是方法不断的调用。方法的调用就是 不断的进行 入栈、出栈。
2. 栈帧 (一个method对应一个栈帧)
虚拟机以方法做为最基本的执行单元,栈帧则是用于虚拟机调用方法的数据结构,他也是虚拟机运行数据区中的栈元素。
栈帧 由 四部分组成:
-
1 ) Local Variable Table —— 局部变量表,相当于寄存器
局部变量表中存放了8种数据类型 boolean 、byte 、char 、int 、 float、reference类型(对象引用)、returnAddress(指向字节码指令的位置)-
reference 表示对一个对象实例的引用,JVM可以用过这个类型做到两件事:
1> 根据引用可以直接或间接找到对象在 Heap 中的起始位置或索引
2> 根据引用可以直接或间接找到对象所属数据类型在方法区存储的类型信息
-
reference 表示对一个对象实例的引用,JVM可以用过这个类型做到两件事:
总结:局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,当方法被调用后,java 虚拟机会使用局部变量表来完成 参数值 到 参数变量列表 的传递过程,即 实参到形参的传递。
-
2 ) Operand Stack —— 操作数栈
当一个方法执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令会往操作数栈中写入和读取内容,也就是 入栈 和 出栈 的操作
操作数栈中的元素类型必须和字节码指令序列严格匹配,在编译程序代码的时候,编译器要严格保守这一点,在类校验阶段还要再次验证
例如整数相加的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入两个int类型的数值,当执行这个指令时,会把两个int值出栈并相加,然后讲相加的结果再入栈
如果操作数栈顶两个元素不全是int类型,有一个为long类型,就不能相加,会出错
总结:java虚拟机栈的解释执行引擎被称为“基于栈的执行引擎”,里面的栈就是 操作数栈
3 ) Dynamic Linking —— 动态连接
每一个栈帧都包含一个指向运行时常量池该所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接4 ) return address —— 方法返回地址
方法执行完之后,都必须回到方法最初被调用时的位置,程序才可以继续运行,方法返回时需要在栈帧中存储一些信息,来帮助他恢复上层主调方法的状态。
a() -> b():方法a调用了方法b,,b方法的返回值放在什么地方,以及回到a方法之后应该从哪里继续执行
每一个方法对应一个栈帧,方法的执行对应着栈帧的入栈,方法的退出对应着栈帧的出栈
每个方法执行的时候,各种参数、变量都存放在局部变量表中,执行的字节码都存放在操作数栈中
三、Natice Method Stack —— 本地方法栈
虚拟机栈为 Java虚拟机执行 Java 方法(字节码) 服务,本地方法栈为虚拟机执行本地(Native)方法服务,Native 修饰的方法 都是 C++实现的。
JVM自动管理,不可人为干预
四、Java 堆 —— Heap
对于Java 程序员来说,堆内存是 JVM 管理的内存中最大的一块,被所有线程共享,在项目启动的时候创建。此内存区域的唯一目的就是存放对象实例,在Java中 几乎 所有的对象实例都在这里分配内存
由于Heap是所有线程共享的,当高并发情况下,可能有多个线程同时想要使用同一内存块,造成线程争用,效率变低。所以从分配内存的角度看,所有线程共享的Heap内存(更细节来说其实是Heap中的Eden区)中可以划分出每个线程私有的分配缓冲区(Thread Local Allocation Buffer,简称TLAB),减少线程争用,以提升对象分配时的效率
无论如何划分,都不会改变Java堆中存储内容的共性——无论哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了GC(垃圾收集器)更好的回收内存,以及更快的分配内存
对象的分配:
尝试栈上分配–》TLAB分配–》堆上分配(新生代、老年代)
五、方法区 —— Area
Method Area——方法区,和Java堆一样被所有线程共享。它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译后的代码缓存等数据
1 )、JDK1.8之前,方法区实现为Perm Space,即永久区
- 字符串常量位于PermSpace
- FGC不会清理
- 大小启动的时候指定,运行之后不能改变,经常会产生内存溢出线程
2 )、JDK1.8之后,方法区实现的名称为Meta Space,即元数据区
- 字符串常量位于堆,不再放在Method Space
- 会触发FGC清理
- 如果不设定的话,最大就是物理内存,也可以指定大小,满了之后就会触发FGC
运行时常量池
Runtime Constant Pool——运行时常量池,是方法区的一部分
我们在前面的学习中知道Class文件结构中有一项信息是常量池表(存放编译器生成的字面量和符号引用(静态概念)),在运行之后(动态概念),这部分内容在类加载完成之后存放到方法区的运行时常量池中。
运行时常量池除了保存Class文件中描述的符号引用外,符号引用翻译出来的直接引用也存储在运行时常量池中
六、直接内存——Direct Memory
直接内存并不是虚拟机运行时数据区的一部分,但是也算是JVM管理的内存,而且这部分内存被频繁的使用,也有可能导致OOM,与运行时数据区关系紧密,所以放在这里一起讲解
为了增加IO效率,在JDK1.4之后增加了直接内存(Direct Memory),实现了从JVM内部能够直接访问操作系统管理的内存,即用户空间能够直接访问内核空间
JDK1.4之后引入了NIO类,引入了基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆(Heap)中的DirectByteBuffer对象作为这块内存的引用进行操作。这样避免了在Java堆(Heap)和Native堆中来回复制数据(即零拷贝)