JVM内存区域基本结构图:
《深入理解Java虚拟机(第二版)》中的描述是下面这个样子的:
1、程序计数器:
程序计数器(Program Counter Register)是当前线程执行的字节码的行号指示器,存放该字节码指令的位置,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
由于Java虚拟机是的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何时刻,一个处理器只执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。
如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。
程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。
特点:
- 每个线程拥有一个PC寄存器
- 在线程创建时创建
- 指向下一条指令的地址
- 执行本地方法时,PC的值为undefined
2、Java虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。栈是线程私有的,他的生命周期与线程相同。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间。
栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。在活动线程中,只有位于栈顶的帧才是有效的,成为当前栈帧。栈帧执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。
Java虚拟机栈特点:
- 线程私有,生命周期和线程相同
- 栈由一系列帧组成(因此Java栈也叫做帧栈)
- 帧保存一个方法的局部变量、操作数栈、常量池指针、方法返回地址
- 每一次方法调用创建一个帧,并压栈
2.1.局部变量表
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型。
- 对象引用:reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置。
- returnAddress:指向了一条字节码指令的地址
局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。
基本数据类型中long和double占用两个Slot,其余的数据类型只占用一个Slot。
Slot 复用:
为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。通过一个例子来理解变量“失效”。
public void test(boolean flag){
if(flag){
int a = 66;
}
int b = 55;
}
当虚拟机运行 test 方法,就会创建一个栈帧,并压入到当前线程的栈中。当运行到 int a = 66时,在当前栈帧的局部变量中创建一个 Slot 存储变量 a,当运行到 int b = 55时,此时已经超出变量 a 的作用域了(变量 a 的作用域在{}所包含的代码块中),此时 a 就失效了,变量a 占用的 Slot 就可以交给b来使用,这就是 Slot 复用。
2.2.操作栈
操作栈是一个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写入和提取信息。JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈。
操作数栈是一个后进先出栈。操作数栈的元素可以是任意的Java数据类型。方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作。通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区。
2.3.动态连接
每个栈帧中包含一个在常量池中对当前方法的引用(指针),目的是支持方法调用过程的动态连接。
2.4.方法返回地址
方法执行时有两种退出情况:
第一,正常退出,即正常执行到任何方法的返回字节码指令,如RETURN、IRETURN、ARETURN等;
第二,异常退出。
无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:
• 返回值压入上层调用栈帧。
• 异常信息抛给能够处理的栈帧。
• PC计数器指向方法调用后的下一条指令。
3、本地方法栈
本地方法栈为虚拟机使用到的Native方法服务,在虚拟机规范中对本地方法栈中实现的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由的实现它。HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
本地方法可以通过JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和JVM相同的能力和权限。
4、Java堆
堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)。
Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。
年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Young GC,清理年轻代内存空间。
老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。
Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。
当生成新的对象时GC流程图:(OOM:OutOfMemoryError)
JVM堆特点:
- 和程序开发密切相关
- 应用系统对象都保存在Java堆中
- 所有线程共享Java堆
- 对分代GC来说,堆也是分代的
- GC管理的主要区域
5、方法区
方法区别名叫做Non-Heap(非堆),同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。
对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多人都愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HostSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样便可以像管理Java堆一样管理这部分内存。
Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
运行时常量池:
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息就是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的常量运行池中存放。
运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
方法区特点:
- 保存装载的类信息,Class对象(jdk1.8之后存储到元空间)
- 类型的常量池
- 字段,方法信息
- 方法字节码
- 通常和永久区(Perm)关联在一起
关于方法区:
jdk1.7之前:
方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;
jdk.7:
存储在永久代的部分数据就已经转移到Java Heap或者Native memory。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native memory;字符串常量池(interned strings)转移到了Java heap;类的静态变量(class statics variables )转移到了Java heap;
jdk1.8:
仍然保留方法区的概念,只不过实现方式不同。取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中。
1)移除了永久代(PermGen),替换为元空间(Metaspace);
2)永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
3)永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
4)永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize:初始化元空间大小,控制发生GC阈值; MaxMetaspaceSize:限制元空间大小上限,防止异常占用过多物理内存)。