【JVM】详解 运行时数据区
目录
程序计数器
虚拟机栈
局部变量表
操作数栈
动态链接(或指向运行时常量池的方法引用)
方法出口
虚方法表
工作机制
本地方法栈
Java堆
逃逸分析
堆空间大小设置
方法区
永久代与元空间
运行时常量池
直接内存
程序计数器
程序计数器(Program Counter Register)记录着当前线程所执行程序的字节码的行号,是一块较小的内存空间,如果线程执行的是本地方法,那么程序计数器的值就为空。字节码解释器通过改变计数器的值选取下一条要执行的指令。
为什么是线程独占的?
一个CPU的核心只能处理一个线程,要想要线程并发工作,就必须要完善线程的上下文切换。程序计数器的作用就是记录线程A在当前程序执行的位置,方便下一次切换到线程A接着上一次的进度继续工作。因此程序计数器必须是线程独占的,方便上下文切换。
是否存在OOM?
是唯一一个在JVM中没有OOM情况的区域
虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack)是线程私有的,它的生命周期与线程相同。用于管理Java程序的运行,保存方法的局部变量,部分结果,并参与方法的调用与返回。
每当一个方法执行,虚拟机栈中也会创建一个栈帧(Stack Frame),栈帧的生命周期与其对应的方法相同,即方法执行时入栈,方法结束时出栈。
栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
是否存在OOM?
如果线程请求的栈深度大于虚拟机规定的栈的深度,将抛出 StackOverflowError 异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。
局部变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类编译器可知的基本数据类型(如int 、 char、 boolean等),对象引用,以及returnAddress类型(指向了一条字节码指令)
最基本的存储单元是slot:用来存储上述的基本数据类型。32位以内的类型只占有一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
局部变量表的大小是在编译期确定下来的,方法运行期间大小不会改变(这里的大小指的是slot的数量)
public class Example {// 实例方法:局部变量表中 slot 0 = this, slot 1 = a, slot 2 = bpublic void instanceMethod(int a, String b) {int c = 10; // slot 3}// 静态方法:局部变量表中 slot 0 = x, slot 1 = ypublic static void staticMethod(long x, Object y) {double z = 1.0; // slot 2}
}
操作数栈
- 用的是数组实现
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 32位类型占用一个栈单位深度,64位占两个
- 不能通过访问索引来访问数据,只能通过入栈和出栈
- 如果存在返回值,那么将返回值压入栈,更新Pc寄存器的下一次指令
栈顶缓存
居于栈式架构的虚拟机使用的零地址指令更加紧凑,但完成一项操作时候必然需要更多的入栈出栈指令,意味着需要更多的指令分派(instruction dispatch)次数和内存读/写次数
频繁的内存读写操作必然会影响执行速度,因此将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率
动态链接(或指向运行时常量池的方法引用)
每一个栈帧内部都包含一个指向 运行时常量池 中该栈帧所属方法的引用。包含这个引用的目的就是实现动态链接。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法符号的引用来表示的。动态链接的作用就是将这些指向符号的引用转换为调用方法的直接引用
方法出口
- 正常执行完成
- 抛出异常
都返回到该方法被调用的位置。正常退出时,返回地址时调用者的pc计数器的值,即调用该方法的指令的下一条指令的地址。抛异常的返回地址要通过异常表来确认。
虚方法表
当一个类包含虚函数时,编译器就会为这个类创建一个虚方法表。虚方法表实际上是一个存储函数指针的数组,这些指针分别指向类中各个虚函数的实际实现。与此同时,每个包含虚函数的类对象都会有一个隐藏的虚表指针(vptr),该指针指向所属类的虚方法表。
工作机制
- 动态绑定的实现:在程序运行时,系统会通过对象的虚表指针找到对应的虚方法表,然后依据调用的方法在表中的索引,定位到实际要执行的函数。
- 多态的支持:要是子类重写了父类的虚函数,那么子类的虚方法表中对应的条目就会指向子类的实现;要是没有重写,就依然指向父类的实现。
本地方法栈
本地方法栈与虚拟机栈的功能基本相同,虚拟机栈为虚拟机执行的Java方法(字节码)服务,本地方法栈为本地方法服务。
是否存在OOM?
本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
Java堆
Java堆用来存放对象实例,是JVM中最大的一块内存区域,被所有线程共享。几乎所有的对象实例都存储在堆中。
Java堆在JVM启动时创建,在逻辑上堆空间是连续的,但也可以分配在不连续的物理空间上。堆既可以是固定大小,也可以是可扩展的。
堆中也可以存在线程独占的空间,即 TLAB(Thread Local Allocation Buffer),是线程私有的分配缓冲区。
是否存在OOM?
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
逃逸分析
当一个对象在方法中被定义且只在方法内部使用,则认为没有发生逃逸
栈上分配:如果没有发生逃逸,则可以将其分配到栈上
同步省略:JIT编译器可以通过逃逸分析判断同步块所用的锁对象是否能够被一个线程访问而不发布到其他线程,编译器就会取消对这部分代码的同步,能提高并发性和性能
分离对象或标量替换:有的对象不需要作为一个连续的内存结构存在也可以被访问到。那么对象的部分或者全部也可以存储在CPU寄存器中
堆空间大小设置
空间大小设置
- 初始空间大小:物理电脑内存大小/64
- 最大内存大小:物理电脑内存大小/4
- 手动:建议将初始与最大内存大小设置成一个值
- 当内存大小超过最大内存大小会抛出OOM
查看GC设置的参数
- jps / jstat -gc 进程id
- -XX:+PrintGCDetails
- -Xms初始内存 -Xmx最大内存
方法区
方法区(Method Area),它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。是被所有线程共享的。
永久代与元空间
永久代属于 JVM 堆内存的一部分,元空间不再属于 JVM 堆内存,而是使用本地内存(Native Memory)。
因为永久代的设计导致方法区很容易OOM,而元空间分配在本地内存中,可利用的空间更大,有效减少OOM的情况。
方法区分配也不需要连续的物理空间,也可以选择固定大小或者可扩展大小,甚至可以选择不进行垃圾回收。
这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。
- jdk1.6 有永久代(permanent generation),静态变量存放在永久代上
- jdk1.7 有永久代,字符串常量池,静态变量移除,保存在堆中
- jdk1.8 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,字符串常量池,静态变量仍在堆
静态变量的本身始终存储在堆中,以上静态变量指的是引用
是否存在OOM?
如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量、符号引用以及把符号引用转换为的直接引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
动态性是运行时常量池的重要特点,运行时也可以将新的常量放入池中。
直接内存
这里需要声明,直接内存不输入JVM运行时数据区。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
是否存在OOM?
动态扩展时当各个内存区域总和大于物理内存限制,会出现OOM。