Java内存区域
Java内存区域
前言
Java内存区域是指 JVM运行时将数据分区域存储 ,简单的说就是不同的数据放在不同的地方。通常又叫运行时数据区域。
一、Java内存区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
如上图所示,运行时数据区被划分成为五块,分别是线程私有区域:程序计数器、java 虚拟机栈、本地方法栈,以及线程共享区域:java堆、方法区。
说明:
Java 虚拟机规范对于运行时数据区域的规定是相当宽松的。以堆为例:堆可以是连续空间,也可以不连续。堆的大小可以固定,也可以在运行时按需扩展 。虚拟机实现者可以使用任何垃圾回收算法管理堆,甚至完全不进行垃圾收集也是可以的。
二、不同版本下Java内存区域的差异
JDK1.7 把原本放在方法区的常量池、静态变量转移到了堆,而到了 JDK1.8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK中永久代还剩余的内容(主要是类信息)全部移到元空间中。
各版本之间的差异如下图:
JDK1.8:
三、运行时数据区域的各自用途
接下来我们来逐一看看这些被划分的区域的用途。
1. 程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。由于JVM可以并发执行线程,因此会存在线程之间的切换,而这个时候就程序计数器会记录下当前程序执行到的位置,以便在其他线程执行完毕后,恢复现场继续执行。
JVM会为每个线程分配一个程序计数器,与线程的生命周期相同。如果线程正在执行的是应该Java方法,这个计数器记录的是正在执行虚拟机字节码指令的地址。如果正在执行的是Native方法,计数器的值则为空(undefined)
程序计数器是唯一一个在《 Java 虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。
2. Java虚拟机栈 (Java Virtual Machine Stack)
2.1 什么是虚拟机栈
虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
虚拟机栈是每个线程独有的,随着线程的创建而存在,线程结束而死亡。
如下图:
虚拟机栈包含很多栈帧,每个方法执行的同时会创建一个栈帧,栈帧又存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
2.2 异常情况
在《Java虚拟机规范》中,对这个内存区域规定了两类异常情况:
1) 栈深度溢出:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常。
2)栈扩展失败:如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
2.3 栈帧的组成
1)局部变量表
局部变量表是存放方法参数和局部变量的区域。
全局变量是放在堆的,有两次赋值的阶段,一次在类加载的准备阶段,赋予系统初始值;另外一次在类加载的初始化阶段,赋予代码定义的初始值。而局部变量没有赋初始值是不能使用的。
2)操作数栈
一个先入后出的栈。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
3)动态连接
每个栈帧都包含一个指向运行时常量池(常量池可以便于指令的识别)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),这也是Java强大的扩展能力,在运行期间才能确定目标方法的直接引用。
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。
4)方法返回地址(方法出口)
返回分为 正常返回 和 异常退出。
无论何种退出情况,都将返回至方法当前被调用的位置,程序才能继续执行。
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。
方法退出的过程相当于弹出当前栈帧。
3. 本地方法栈(Native Method Stacks)
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别是Java虚拟机栈是调用Java方法;本地方法栈是调用本地native方法,可以认为是通过 JNI (Java Native Interface) 直接调用本地 C/C++ 库,不受JVM控制。
如下图:
异常情况:
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverFlowError和OutOfMemoryError异常。
二、堆 heap
1. Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
2. 堆是垃圾收集器管理的主要区域,又称为“GC堆”,可以说是Java虚拟机管理的内存中最大的一块。
3. 现在的虚拟机(包括HotSpot VM)都是采用分代回收算法,详情请参考文章《垃圾回收算法-通用的分代垃圾回收机制》。在分代回收的思想中, 把堆分为:新生代+老年代+永久代(1.8没有了); 新生代 又分为 Eden + From Survivor + To Survivor区。
三、方法区method area
1. 方法区与 Java 堆一样,是所有线程共享的内存区域。
2. 方法区用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、即时编译器编译后的代码缓存等数据。
3. 方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”(Non-Heap)。
4. 方法区比较重要的一部分是运行时常量池(Runtime Constant Pool),为什么叫运行时常量池呢?是因为运行期间可能会把新的常量放入池中,比如说常见的String的intern()方法。
运行时常量池是方法区的重要组成部分,主要用于存放类的编译期生成的各种字面量(如字符串、数字等)和符号引用(如类名、方法名、字段名等)。这些常量在编译时由字节码生成,并在类加载时放入运行时常量池中,供 JVM 在运行时使用。
然而,之所以称为“运行时常量池”,是因为它不仅仅存储编译期的常量,还允许在运行时向常量池动态添加内容。
四、举个例子
我们看下在Java程序运行过程中,各内存区域是如何工作的?
参考链接:
https://javaguide.cn/java/jvm/memory-area.html