一、运行时数据区域
Java 虚拟机在执行 Java 程序的过程中,会把它管理的内存划分成若干个不同的数据区域。
JDK 1.8 和之前的版本略有不同,这里介绍 JDK 1.7 和 JDK 1.8 两个版本。
JDK 1.7:
线程私有:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享:
- 堆
- 方法区
- 直接内存
JDK 1.8
线程私有:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享:
- 堆
- 直接内存
- 元空间
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳跃、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储。
虚拟机栈
线程私有,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。
除了一些 Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过栈来实现。
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
局部变量表
主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用。
操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态链接
主要服务一个方法需要调用其他方法的场景。Class文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为动态链接。
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在的收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
JDK 7及之前的版本,堆内存通常分为以下三部分:
- 新生代(Young Generation)
- 老生代(Old Generation)
- 永久代(Permanent Generation)
下图的 Eden 区、两个Survivor 区 S0 和S1都属于新生代,中间一层属于老年代,最下面一层属于永久代。
JDK 8 版本及以后永久代(Permanent Generation)已被元空间(MetaSpace)取代,元空间使用的是本地内存。
大部分情况,对象首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄会加1(Eden 区 -> Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会晋升到老年代中。
为什么年龄只能是0-15?
因为记录年龄的区域在对象头中,这个区域的大小通常是4位,这4位可以表示的最大二进制数字是1111,即十进制的15。因此,对象的年龄被限制为0到15。
方法区
当虚拟机要使用一个类时,他需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
运行时常量池
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译器生成的各种字面量和符号引用的常量池表。
字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
字符串常量池
字符串常量池是 JVM 为了提升性能和减少内存开销针对字符串专门开辟的一块区域,主要目的是为了避免字符串重复创建。