目录
一、程序计数器
二、虚拟机栈
3、本地方法栈
4、Java堆(Heap)
5、方法区
很多人将 JAVA 内存分为堆内存(Heap)和栈内存(Stack),这种划分方式在一定程度上体现这两块区域是 Java 工程师最关注的内存区域。但是这种划分方式并不完全正确,Java 的内存区域划分实际上远比这复杂,Java 虚拟机在执行 Java 程序的过程中,会把他管理的内存划分为不同的数据区域。如下图
一个 HelloWorld.java 程序被编译为 HelloWorld.class 文件后,由类加载器 ClassLoader 加载 JVM 的内存中。JVM 中的内存可以划分为若干个不同的数据区:程序计数器,虚拟机栈,本地方法栈,堆和方法区。
Java程序是多线程的,CPU 可以在多个线程中分配执行实际片段。
作用:当某个线程被 CPU 挂起时,需要记录当前代码已经执行到的位置,方便 CPU 重新执行此线程时,知道重那行指令开始执行。
程序计数器是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。
程序计数器的几点注意:
1. 在 Java 虚拟机规范中对程序计数器这一区域没有规定任何的 OutOfMemoryError 情况。
2. 程序计数器是线程私有的数据区,每个线程内部都有一个私有的程序计数器,它的生命周期随着线程的创建而创建,随着线程的结束而消亡。
3. 当一个线程正在执行一个 Java 方法时,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是 Native 方法,这个计数器值则为空。
虚拟机栈也是线程私有的,与线程的生命周期同步。在 Java 虚拟机规范中,对这个区域规定了两种异常情况:1)StackOverflowError: 当线程请求栈深度超出虚拟机栈所允许的深度时抛出;2) OutOfMemoryError: 当 Java 虚拟机动态扩容到无法申请足够内存时抛出。
JVM 是基于栈的解释器执行的,这里的“栈”指的就是虚拟机栈。DVM 是基于寄存器解释器执行的。虚拟机栈的初衷是用来描述 Java 方法执行的内存模型。每个方法被执行的时候,JVM 都会在虚拟机张中创建一个栈帧。
栈帧
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,每个线程在执行某个方法时,都会为这个方法创建一个栈帧。一个线程包含多个栈帧(多个方法),而每个栈帧内容包含:局部变量表,操作数栈,动态连接和返回地址。
局部变量表:是变量值的存储空间。调用方法时传递的参数,在方法内存创建的局部变量都保存在局部变量表中。在 Java 编译成 class 文件的时候,会在方法的 Code 属性表中的 max_locals 数据项中确定该方法需要分配的最大局部变量表的容量。
该程序编译成 class 文件,使用 javap -v 反编译成字节码指令
里面的 "locals = 3",就代表局部变量表的长度为3,分别保存参数k, 局部变量i ,j。
操作数栈:常称为操作栈,它是一个后人先出栈(LIFO)。操作数栈的最大深度也在编译的时候写入方法的 Code 属性表中的 max_stacks 数据项中。栈中的元素可以是任意 Java 数据类型,包括 long 和 double。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令被压入和弹出操作数栈。
动态链接:主要目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存放在 方法区中。Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用(每个栈帧包含一个符号引用-->该栈所属的方法)。
返回地址:当一个方法开始执行后,只有两种方式可以退出这个方法:1)正常退出:指方法中的代码正常完成,或者遇到任意一个方法返回的字节码指令(如return)并退出没有抛出任何异常;2)指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。
虚拟机栈中的返回地址是用来帮助当前方法恢复它的上层方法执行状态。正常退出时,调用者的 PC 计数值可以作为放回地址,栈帧中可能保存此计数值;异常退出时,放回地址是通过异常处理器表确定的,栈帧中一般不会保存此信息。
本地方法栈和虚拟机栈级别相同,是针对本地(native)方法。在开发中如果涉及 JNI 可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一(比如 HotSpot)。
是 JVM 所管理的内存中最大的一块,该区域唯一的目的就出存放对象实例。
是 Java 垃圾收集器(GC)管理的主要区域,有时候也叫做 “GC堆”。
是所有线程共享的内存区域。被分配在此区域的对象如果被多个线程访问,需要考虑线程安全问题。
按照对象存储时间的不同,堆中的内存可以划分为新生代、老年代,其中新生代右被划分为 Eden和 Survivor 区。不同的区域存放具有不同生命周期的对象,这样可以根据不同的区域使用不同的垃圾回收算法。
方法区是 JVM 规范里规定的一块运行时数据区。主要存储:已经被 JVM 加载的类信息(版本,字段,方法,接口);常量;静态变量;即时编译器编译后的代码;数据。该区域是被各个线程共享的内存区域。
方法区与永久区:
方法区是规范层面的东西,规定了这一区域要存放哪些数据;永久区或者是 metaspace 是对方法区的不同实现,是实现层面的东西。