大家好,我们又见面了。有小伙伴说 JVM 内存区域在学习与面试的时候常常理不清,为了解决这位小伙伴的困扰,我将通过两篇文章为大家理清 JVM 内存区域划分,这篇是第一篇将为大家介绍 JVM 内存区域的逻辑概念,将和大家唠一唠 HotSpot 虚拟机实现的一些小细节。安全带系好,发车咯!
首先我们来明确一下 运行时数据区 的概念,Java 虚拟机在执行 Java 程序的过程中会将它所管理的内存划分为若干不同的数据区,这些数据区就是运行时数据区,这些区域有各自的用途和生命周期。
根据《Java 虚拟机规范》的规定 JVM 所管理的内存分为:虚拟机栈、本地方法栈、程序计数器、方法区和堆这五种运行时数据区。
下面我们来分别介绍他们的逻辑概念。
需要注意,逻辑概念只是《Java虚拟机规范》中对运行时数据区进行的逻辑规范,不同的虚拟机的具体实现会略有差异。
虚拟机栈也是我们常说的 Java 栈,是线程私有的,在每一个线程启动的时候都会创建一个虚拟机栈。
虚拟机栈的生命周期和线程一致,虚拟机栈的内部存储着一个个栈帧,对应着 Java 方法的一次次调用,方法被调用则压栈,方法调用结束则出栈。
每一个栈帧内部又存储了局部变量表、方法返回地址、操作数栈、动态链接等信息。
在虚拟机栈规范了两种异常情况。一是当线程请求的栈深度大于虚拟机所允许的深度则会抛出 SOF(StackOverflowError)异常;二是当虚拟机栈容量动态扩展时无法申请到足够的内存则会抛出 OOM(OutOfMemoryError)异常。
本地方法栈的作用与虚拟机栈类似,只不过虚拟机栈服务于 Java 方法调用,本地方法栈服务于本地方法(native)调用。
本地方法栈也是线程私有的,且与虚拟机栈一致,在栈深度异常和扩展失败时分别会抛出 SOF 异常和 OOM 异常。
程序计数器用作当前线程所执行字节码的行号指示器,同样是线程私有的,它会存储当前正在执行的字节码指令地址,并在当前字节码指令执行结束后切换为下一步需执行的字节码指令地址,线程就在程序计数器的推动下一步步执行。
如果当前执行的是本地方法,则程序计数器会存储 Undefined。
方法区是各个线程共享的一块内存区域,主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码缓存等数据。
JVM 关闭后方法区将会被释放。
方法区的常量主要被划分为运行时常量池和字符串常量池两大区域。
字节码文件中有一个区域叫做常量池表(Constant Pool Table),常量池表主要用于存储编译器生成的各种字面量与符号引用。
常量池表可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
而常量池表存储的内容将在类加载后存放到方法区的运行时常量池中。
至于字符串常量池就是用于储存字符串。
需要注意 Java 中的基本类型的包装类的大部分都实现了常量池技术,不过这里的常量池严格来说应该叫做对象池,所以它们归属于堆,并不属于方法区。
无论在不同版本的虚拟机实现中字符串常量池和静态变量的存储位置发生了怎样的变化,在逻辑上这两个区域是永远归属于方法区的。
方法区在内存扩展失败的时候同样会抛出 OOM 异常。
堆和方法区一样也是各个线程共享的一块内存区域。堆也就是我们常说的 Java 堆,也是垃圾回收器主要管理的区域(方法区可以选择不实现垃圾收集),“几乎”所有的对象实例都在这里分配内存。
在规范中,堆可以处于在物理上不连续的内存空间,只要逻辑上连续即可,不过对于数组这种大对象,为了实现简单和提高性能,大多数虚拟机实现都会考虑使用连续的内存空间。
在当前主流的虚拟机实现中,堆都可以通过-Xms 和-Xmx 设置容量,如果堆中的内存不足以完成对象的实例分配,且堆无法再扩展时就会抛出 OOM 异常,这也是 OOM 异常最频发的一块区域。