JVM 内存结构

JVM 内存结构

前面介绍了内存的不同形态:物理内存和虚拟内存。介绍了内存的使用形式:内核空

间和用户空间。接着又介绍了Java 有哪些组件需要使用内存。下面着重介绍在JVM 中是

如何使用内存的。

JVM 是按照运行时数据的存储结构来划分内存结构的,JVM 在运行Java 程序时,将

它们划分成几种不同格式的数据,分别存储在不同的区域,这些数据统一称为运行时数据

(Runtime Data)。运行时数据包括Java 程序本身的数据信息和JVM 运行Java 程序需要的

额外数据信息,如要记录当前程序指令执行的指针(又称为PC 指针)等。

在 Java 虚拟机规范中将Java 运行时数据划分为6 种,分别为:

◎ PC 寄存器数据;

Java 栈;

◎ 堆;

◎ 方法区;

◎ 本地方法区;

◎ 运行时常量池。

PC 寄存器

PC 寄存器严格来说是一个数据结构,它用于保存当前正常执行的程序的内存地址。

同时Java 程序是多线程执行的,所以不可能一直都按照线性执行下去,当有多个线程交

叉执行时,被中断线程的程序当前执行到哪条的内存地址必然要保存下来,以便于它被恢

复执行时再按照被中断时的指令地址继续执行下去。这很好理解,它就像一个记事员一样

记录下哪个线程当前执行到哪条指令了。

但是 JVM 规范只定义了Java 方法需要记录指针信息,而对于Native 方法,并没有要

求记录执行的指针地址。

Java 栈

Java 栈总是和线程关联在一起,每当创建一个线程时,JVM 就会为这个线程创建一个

对应的Java 栈,在这个Java 栈中又会含有多个栈帧(Frames),这些栈帧是与每个方法关

联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些内部变量(在方法内定

义的变量)、操作栈和方法返回值等信息。

每当一个方法执行完成时,这个栈帧就会弹出栈帧的元素作为这个方法的返回值,并

清除这个栈帧,Java 栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的

方法,PC 寄存器也会指向这个地址。只有这个活动的栈帧的本地变量可以被操作栈使用,

当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧又被创建,这个新创建的

栈帧又被放到Java 栈的顶部,变为当前的活动栈帧。同样现在只有这个栈帧的本地变量

才能被使用,当在这个栈帧中所有指令执行完成时这个栈帧移出Java 栈,刚才的那个栈

帧又变为活动栈帧,前面的栈帧的返回值又变为这个栈帧的操作栈中的一个操作数。如果

前面的栈帧没有返回值,那么当前的栈帧的操作栈的操作数没有变化。

由于 Java 栈是与Java 线程对应起来的,这个数据不是线程共享的,所以我们不用关

心它的数据一致性问题,也不会存在同步锁的问题。

堆是存储 Java 对象的地方,它是JVM 管理Java 对象的核心存储区域,堆是Java 程

序员最应该关心的,因为它是我们的应用程序与内存关系最密切的存储区域。

每一个存储在堆中的Java 对象都会是这个对象的类的一个副本,它会复制包括继承

自它父类的所有非静态属性。

堆是被所有Java 线程所共享的,所以对它的访问需要注意同步问题,方法和对应的

属性都需要保证一致性。

方法区

JVM 方法区是用于存储类结构信息的地方,如在第7 章介绍的将一个class 文件解析

成JVM 能识别的几个部分,这些不同的部分在这个class 被加载到JVM 时,会被存储在

不同的数据结构中,其中的常量池、域、方法数据、方法体、构造函数,包括类中的专用

方法、实例初始化、接口初始化都存储在这个区域。

方法区这个存储区域也属于后面介绍的Java 堆中的一部分,也就是我们通常所说的

Java 堆中的永久区。这个区域可以被所有的线程共享,并且它的大小可以通过参数来设置。

这个方法区存储区域的大小一般在程序启动后的一段时间内就是固定的了,JVM 运行

一段时间后,需要加载的类通常都已经加载到JVM 中了。但是有一种情况是需要注意的,

那就是在项目中如果存在对类的动态编译,而且是同样一个类的多次编译,那么需要观察

方法区的大小是否能满足类存储。

方法区这个区域有点特殊,由于它不像其他Java 堆一样会频繁地被GC 回收器回收,

它存储的信息相对比较稳定,但是它仍然占用了Java 堆的空间,所以仍然会被JVM 的GC

回收器来管理。在一些特殊的场合下,有时通常需要缓存一块内容,这个内容也很少变动,

但是如果把它置于Java 堆中它会不停地被GC 回收器扫描,直到经过很长的时间后会进入

Old 区。在这种情况下,通常是能控制这个缓存区域中数据的生命周期的,我们不希望它

被JVM 内存管理,但是又希望它在内存中。面对这种情况,淘宝正在开发一种技术用于

在JVM 中分配另外一个内存存储区域,它不需要GC 回收器来回收,但是可以和其他内

存中对象一样来使用。

运行时常量池

在 JVM 规范中是这样定义运行时常量池这个数据结构的:Runtime Constant Pool 代表运

行时每个class 文件中的常量表。它包括几种常量:编译期的数字常量、方法或者域的引用

(在运行时解析)。Runtime Constant Pool 的功能类似于传统编程语言的符号表,尽管它包含

的数据比典型的符号表要丰富得多。每个Runtime Constant pool 都是在JVM 的Method area

中分配的,每个Class 或者Interface 的Constant Pool 都是在JVM 创建class 或接口时创建的。

上面的描述可能使你有点迷惑,这个常量池与前面方法区的常量池是否是一回事?答

案是肯定的。它是方法区的一部分,所以它的存储也受方法区的规范约束,如果常量池无

法分配,同样会抛出OutOfMemoryError。

本地方法栈

本地方法栈是为JVM 运行Native 方法准备的空间,它和前面介绍的Java 栈的作用是

类似的,由于很多Native 方法都是用C 语言实现的,所以它通常又叫C 栈,除了在我们

的代码中包含的常规的Native 方法会使用这个存储空间,在JVM 利用JIT 技术时会将一

些Java 方法重新编译为Native Code 代码,这些编译后的本地代码通常也是利用这个栈来

跟踪方法的执行状态的。

在 JVM 规范中没有对这个区域的严格限制,它可以由不同的JVM 实现者自由实现,

但是它和其他存储区一样也会抛出OutOfMemoryError 和StackOverflowError。

相关推荐