深入理解 JVM(一)Java的内存区域

JVM 作为 Java 学习的核心概念,承载了 Java,compile once run everywhere 的理念。理解 JVM 是每一个 Java DEV 的必修课。
Java 的虚拟机会在执行 Java 程序的过程中,把其所管理的内存划分为若干不同的数据区域。在本篇文章中,通过对《深入理解Java虚拟机 JVM高级特性与最佳实践》一书的阅读,做以下知识点的梳理与总结。

JVM 内存管理区域

运行时数据区域

程序计数器(Program Counter Register)

程序计数器是一个比较小的内存区域,用于线程所执行字节码的行号指示器,字节码解释器通过改变此值选取下一条字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖于此。
每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native ,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError 的区域。



Java 虚拟机栈(VM Stack)

一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame ),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM 栈中入栈,当方法执行完成时,栈帧出栈。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long 和double 类型会占用2个局部变量空间(Slot ,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java 虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。
每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。



本地方法栈(Native Method Stack)

本地方法栈与虚拟机栈作用相似,虚拟机栈为虚拟机执行 Java 方法(字节码)服务,而本地方法栈为虚拟机栈使用的 Native 方法服务。

Java 堆(Heap)

堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的是存放对象实例以及数组
Java 堆可以处于物理上的不连续内存空间,只要逻辑连续即可。
Java 堆大小可固定也可以扩展。
堆是垃圾收集器管理的主要区域,关于垃圾收集,将在之后的文章中详细介绍。

方法区(Method Area)

线程共享。
方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field 、方法、接口等信息)、final 常量、静态变量、编译器即时编译的代码等。
曾被称作为永久代,存放字符串常量,JDK 1.7 HotSpot 中,已经移出方法区。
方法区有针对常量池和类型对象的卸载的内存回收管理。
在方法区上定义了OutOfMemoryError:PermGen space 异常,在内存不足时抛出。

运行时常量池(Runtime Constant Pool)

方法区的一部分,Class 文件中除了类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table ),用于存放编译期生成的各种字面量 literal 和符号引用 Symbolic References(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译),这些将在类加载后进入常量池。
一般除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
具有动态性,可在运行期间将新的常量放入池中

运行时常量池需区分于 Class 文件常量池,Class 文件常量池是在编译为 .class 文件时,便确定的常理。常理类型也有字面量和符号引用。而运行时常量池是在类加载后运行时动态加入常量池的,具有动态性。


非运行时数据区域

直接内存(Direct Memory)

直接内存并不是JVM 管理的内存,可以这样理解,直接内存,就是JVM 以外的机器内存,比如,你有4G的内存,JVM 占用了1G,则其余的3G就是直接内存。
JDK 1.4 中新加入了 NIO(New Input/Output) 类,引入了一种基于通道(Channel )与缓冲区(Buffer )的I/O 方式,可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。

参考资料

  • 《深入理解 Java 虚拟机 JVM高级特性与最佳实践》(第2版),周志明著
作者

Wenchao Wang

发布于

2017-05-17

更新于

2020-10-17

许可协议

You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.