深入理解JVM - 运行时数据区域
Java虚拟机在执行Java程序时会将其管理的内存分为若干区域。
程序计数器 Program Counter Register
- 当前线程所执行的字节码的行号指示器。类似寄存器PC,在JVM的概念模型中,就是通过改变这个计数器的值来选取下一条字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成
- 线程私有。每个线程都有自己独立的程序计数器,或者说线程切换时切换前的线程要能保存自己的计数器值,切换后的线程要能恢复自己的计数器值,各线程计数器互不影响。
- 如果线程正在执行Java方法,则计数器值为正在执行的虚拟机字节码指令地址;若正在执行本地(Native)方法,则计数器值未定义(Undefined)
- 唯一一个在《JVM规范》中未规定任何OOM情况的区域。
概念模型:代表了所有虚拟机统一外观,但各虚拟机并不一定完全按照概念模型定义来设计,可能通过更高效率的等价方式实现。
Java虚拟机栈 Java Virtual Machine Stack
- 线程私有
- 描述Java方法执行的线程内存模型。一个方法的执行和退出对应一个栈帧的入栈和出栈。
- 栈帧。存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 局部变量表。存放编译期已知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress,这些数据类型在局部变量表中以局部变量槽(slot)表示,64位的long和double占用两个变量槽,其余类型占用一个。局部变量表所需内存空间在编译期完成分配,在进入一个方法时,这个方法需要在栈帧中分配多大局部变量空间是完全确定的(非栈帧大小是确定的),运行时不会改变局部变量表大小(槽数量)。
- SOF:请求栈深度大于虚拟机所允许的深度(如何理解)
- OOM:若Java虚拟机栈容量可以动态扩展(HotSpot不能),当栈扩展时无法申请到足够内存。即便不能动态扩展,但如果申请栈空间时就失败也会导致OOM
本地方法栈 Native Method Stacks
与虚拟机栈的作用类似,不过本地方法栈为本地方法服务,Java虚拟机栈为Java方法(字节码)服务。
《JVM规范》未对本地方法栈使用的语言、使用方式、数据结构进行强制规定,因此虚拟机可灵活实现。HotSpot虚拟机将本地方法栈和虚拟机栈合二为一实现
和虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
Java堆 Java Heap
- 虚拟机所管理的内存中最大的一块
- 线程共享,虚拟机启动时创建
- 唯一的目的就是存放对象实例,"几乎"所有对象实例及数组都在堆中分配。由于即时编译技术的进步,尤其逃逸分析技术的日渐强大,栈上分配、标量替换优化手段导致一些对象不再堆中分配。
- 可划分多个线程私有的分配缓存区(Thread L:ocal Allocation Buffer,TLAB),提升对象分配效率。
- 《JVM规范》规定,Java堆可物理上不连续,但逻辑上必须连续,多数虚拟机处于实现简单、存储高效的考虑,可能要求物理上也连续。
- 可实现为固定大小,也可实现为可扩展。
- OOM:在Java堆中没有内存完成实例分配,且堆也无法再扩展时,JVM抛出OOM
方法区 Method Area
- 线程共享
- 存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- 永久代:Permnent Generation,并非《JVM规范》中的概念,仅是之前HotSpot虚拟机设计团队将收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区,从而省去专门为方法区编写内存管理代码。对于其他虚拟机,是不存在永久代的概念的。永久代的设计导致Java应用更容易导致内存溢出的问题,以及少数方法(如String#intern)在不同虚拟机下有不同表现。
- JDK7:HotSpot将原本放在永久代的字符串常量池、静态变量移出
- JDK8:完全废弃永久代,使用本地内存实现的元空间(Metaspace)代替,将永久代剩余内容(主要是类型信息)移到元空间。
- 《JVM规范》约束:很宽松,不需要物理上连续内存、可选固定大小或可扩展,还可以不实现垃圾收集。
- 方法区的回收主要是常量池的回收和对类型的卸载。
运行时常量池 Runtime Constant Pool
- 方法区一部分
- Class文件中除了类版本、字段、方法、接口等描述信息,还有一项是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,在类加载后放到方法区的运行时常量池中。
- 《JVM规范》对于Class文件的每个部分(包括常量池)的格式有严格要求,但是对运行时常量池没有限制。一般来说,除了保存Class文件中描述的符号引用,还会把由符号引用翻译出来的直接引用也存储在运行时常量池。
- 与Class文件常量池相比,运行时常量池的特点是动态性,也就是不要求一定是编译期产生的,如 String#intern
- OOM:受到方法区限制,无法申请到足够内存抛出OOM
直接内存 Direct Memory
- 非运行时数据区域一部分,非《JVM规范》定义区域
- JDK1.4引入NIO,可使用Native函数库直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象引用这块内存,避免Java堆和Native堆中来回复制数据(如何理解)。
- OOM:不受Jav堆大小限制,但受本地内存限制,申请内存时无足够内存也会抛出OOM
问题
- C/C++的堆和栈,Java的堆和栈
- 栈帧、局部变量表、操作数栈
- 基于寄存器的虚拟机与基于栈的虚拟机
- 栈深度溢出?栈扩展失败?栈?
- 逃逸分析技术?栈上分配、标量替换?
- 常量池、运行时常量池、字符串常量池?
- 符号引用与直接引用?
- Java堆与Native堆?