JVM内存管理
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域,JDK 1.8 和之前的版本的数据区域有所差异,JDK1.6如下图所示。
图片来源:JavaGuide公众号
Java内存布局共有 5 大块,它们分别是堆区(Java Heap)、虚拟机栈(Virtual Machine Stacks)、本地方法栈(Native Method Stacks)、元空间(Meta Spaces)、程序计数器(Program Counter Register)。
线程私有的:
- 程序计数器: 当前线程执行的字节码的位置指示器
- 虚拟机栈(栈内存):保存局部变量,基本数据类型以及堆内存中对象的引用变量
- 本地方法栈(C栈):为JVM提供使用native方法的服务
线程共享的:
- 堆(线程共享): 垃圾回收的主要场地
- 方法区(线程共享): 常量静态变量 JIT(即时编译器)编译后代码也在方法区存放
- 直接内存 (非运行时数据区的一部分)
1. 程序计数器
特点:
- 占用的 JVM 内存空间较小
- 每个线程生命周期内独享自己的程序计数器(内部存放的是字节码指令的地址引用)
- 不会发生 OOM
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。
此外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
PS:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2. 虚拟机栈
特点:
- 内部结构是栈帧,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法返回地址等信息
- 某方法在调用另一个方法是通过动态链接在常量池中查询方法的引用,进而完成方法调用
- 某方法在调用另一个方法的过程,即是一个栈帧在虚拟机中的入栈到出栈的过程
- 虚拟机中的方法入栈的顺序和方法的调用顺序是一致的
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
局部变量表
主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java 虚拟机栈会出现两种异常:*Error
和 OutOfMemoryError
。
-
*Error
: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 *Error 错误。 -
OutOfMemoryError
: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常
Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
Java 栈可以类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java 方法有两种返回方式:
- return 语句。
- 抛出异常。
不管哪种返回方式都会导致栈帧被弹出。
对于 JVM 中虚拟机栈参数的设置
3.本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 *Error
和 OutOfMemoryError
两种错误。
4. 堆
特点:
- 存放Java对象和数组
- 虚拟机中存储空间比较大的区域
- 可能出现OOM异常区域
- 该区域是GC主要区域,堆区由年轻代和老年代组成,年轻代又分为Fden区、S0区(from survivor)、S1区(to survivor);新生代对应Minor GC(Young GC)、老年代对应Full GC(Old GC)
Java虚拟机管理的内存中最大的区域,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此被称为GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法、所以Java堆还可以细分为:新生代和老年代
永久带说明:
Jdk1.6及之前:常量池分配在永久代。
Jdk1.7:有,但已经逐步“去永久代” 。
Jdk1.8及之后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中),如下所示:
在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
5. 方法区
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
运行时常量池:
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用),既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,
当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
方法区被所有线程共享。采用永久代的方式实现了方法区。l jdk 8 以前(不包括 jdk8)存在永久代(Perm区),jdk 8 以后(包括 jdk 8)移除了永久代。如下图所示: