前言:
我们每天都在编写 Java 代码, 编译, 执行. 很多人已经知道 Java 源代码文件 (.java 后缀) 会被 Java 编译器编译为字节码文件(.class 后缀), 然后由 JVM 中的类加载器加载各个类的字节码文件, 加载完毕之后, 交由 JVM 执行引擎执行.
那在整个程序执行过程中, JVM 中怎么存取数据和相关信息呢?
事实上在 JVM 中是用一段空间来存储程序执行期间需要用到的数据和相关信息, 这段空间一般被称作为 Runtime Data Area(运行时数据区), 也就是我们常说的 JVM 内存.
一, 运行时数据区域包括哪些?
image
根据《Java 虚拟机规范》的规定, 运行时数据区通常包括这几个部分: 程序计数器(Program Counter Register),Java 虚拟机栈(Java Vitual Machine Stack), 本地方法栈(Native Method Stack), 方法区(Method Area), 堆(Heap).
二, 各个部分存储的信息和负责的职能
1, 程序计数器
这个内存区域是 Java 虚拟机规范中唯一一个没有规定任何 OOM(OutOfMemoryError)情况的区域, 这是这个区域最大的特点之一, 这是因为程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变, 因此, 对于程序计数器是不会发生内存溢出现象 (OutOfMemory) 的.
这个区域主要是负责记录正在执行的虚拟机字节码指令地址, 即当前线程执行的字节码的行号指示器(注意: JVM 不是直接执行 Java 代码, 而是执行. class 文件, 所以只要其他编程语言能翻译成. class 文件一样能放入 JVM 中执行).JVM 会给每个线程一个独立的程序计数器, 计数器之间互不影响, 且通过线程轮流切换并且分配处理器执行时间来实现 JVM 的多线程. 不过当线程执行的是 Native 方法的时候这个计数器中的值为 undefined.
2,Java 虚拟机栈
和程序计数器一样的是 Java 虚拟机栈是线程私有, 生命周期和线程相同. 虚拟机栈描述的是 Java 方法执行的内存模型: 每个方法在执行的时候都会创建栈帧, 用来存储局部变量表, 操作数栈, 动态链接, 方法出口等信息, 每个方法从调用到执行完成的过程, 就对应一个栈帧在虚拟机中入栈到出栈的过程, 其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间, 其余的数据类型只占用 1 个. 这里需要理解一下的就是为什么要用栈这个结构呢, 比如 A 方法中调用了 B 方法, 虚拟机中是先让 A 方法的栈帧进入虚拟机栈执行, 当执行到调用 B 方法的语句就让 B 栈帧进入, 执行完之后 B 栈帧就出栈, A 栈就继续执行. 这里注意的是如果递归的方法递归的太深很容易抛出下面两种异常, 所以递归虽然写起来方便, 但是性能会有所下降, 并且容易抛出异常.
Java 虚拟机规范中, 对这个区域规定了两种异常状况
i. 线程请求栈的深度大于虚拟机所允许栈的深度, 将抛出 Stack Overflow Error
ii. 如果虚拟机栈可以动态扩展且扩展时无法申请到足够的内存, 会抛出 OutOfMemoryError
3, 本地方法栈
与虚拟机栈作用相似, 不过是虚拟机栈为虚拟机执行 Java 方法提供, 而本地方法为虚拟机使用到的 Native 方法服务, Native 方法多是用 C++ 写的. 抛出的异常和虚拟机栈相同.
4,Java 堆
Java 堆是与前面的区域不同的是: 这个区域是被所有线程共享的一块内存区域, 用来存放对象实例, 并为对象实例分配好内存. Java 虚拟机规范中这样描述: 所有对象实例以及数组都要在堆上分配 Java 堆也是垃圾收集器管理的主要区域, 也叫 "GC 堆". 由于现在的垃圾回收算法多是分代收集, 所以 Java 堆里面又可分为: 新生代和老年代. 并且根据 Java 虚拟机规范的规定: Java 堆可以处于物理上不连续的内存空间中, 只要逻辑上连续即可. 有实例没有被分配, 且堆无法再扩展的时候会抛出 OutOfMemoryError 异常, 虚拟机调优其实也主要关注的是这个区域.
5, 方法区
与 Java 堆一样, 线程共享, 用来存储被虚拟机加载的类信息, 常量, 静态变量. 这个区域 Java 虚拟机规范对其特别宽松, 既可以像 Java 堆那样不需要连续内存, 又可以选择固定大小和可扩展. 还可以选择不实现垃圾收集, 这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载. 当无法满足内存分配需求时, 将抛出 OutOfMemoryError 异常.
目前虚拟机 Hotspot 已经将这部分存储空间从使用 JVM 内存换成使用本地内存, 即这部分不再叫永久代, 而是元空间. 这个元空间实际上是 JVM 动态规定内存大小. 这个替换有什么优势呢? 因为字符串常量池是存在永久代中, 很容易出现性能问题, 并且类和方法信息大小难确定, 给永久代的的大小指定带来困难, 而且 GC 会对永久代特殊处理, 这就增加了 GC 的复杂性. 从 JDK1.7 开始, 字符串常量池就划分进了堆中, 其他的更多是元空间在内存划分的算法上更趋于合理
6, 运行时常量池
是方法区的一部分. 用于存放编译期生成的各种字面量和符号引用, 同时也会把翻译出来的直接引用也存储在运行时的常量池中, 具有动态性. 常量不一定只有编译期才能产生, 运行期间也可以将新的常量放入池中. 例如 String 的 Intern()方法. 同样抛出 OutOfMemoryError 异常
三, 直接内存
这个区域并不是属于运行时数据区域, 但是这个区域也会被频繁使用, 并且抛出 OOM 异常. 这个区域主要是由于在 JDK1.4 中新加入了 NIO(New Input/Output)类, 引入了一种基于通道与缓冲区的 I/O 方式, 它可以使用 Native 函数库直接分配堆外内存, 通过一个储存在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作. 这样能避免在 Java 堆和 Native 堆中来回复制数据, 从而在一些场景中显著提高性能. 直接内存分配不会受到 Java 堆大小的限制, 会受到本机总内存大小及处理器寻址空间的限制. 会抛出 OutOfMemoryError 异常
四, 总结
只有程序计数器不会报出任何相关 OOM 异常, 而 Java 虚拟机栈有可能会报出 OOM 或 Stack Overflow 异常. Java 虚拟机栈主要是存储方法的一些信息, 能让方法顺利的执行, 而 Java 堆存储的是对象的信息. 虚拟机的垃圾回收算法主要在这一块, 并且平常调优的区域也是在这一块.
来源: http://www.jianshu.com/p/70d294e4ea9c