JVM 内存结构
- 运行时数据区
- 一、程序计数器(线程私有)
- 二、虚拟机栈(线程私有)
- 三、本地方法栈(线程私有)
- 四、堆内存(线程共享)
- 五、方法区(线程共享)
运行时数据区
内存是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行。不同的 JVM 对于内存的划分方式和管理机制存在着部分差异。下图是 JVM 整体架构,中间部分就是 Java 虚拟机定义的各种运行时数据区域。
Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。
线程私有:程序计数器、虚拟机栈、本地方法区
线程共享:堆、方法区,堆外内存(JDK 7的永久代或JDK 8的元空间、代码缓存)
一、程序计数器(线程私有)
PC 寄存器用来存储指向下一条指令的地址。由执行引擎读取下一条指令。
它是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域。
二、虚拟机栈(线程私有)
每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,生命周期和线程一致。不存在垃圾回收问题。
作用:主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈中可能出现的异常:
JVM允许虚拟机栈的大小是动态的或者是固定不变的,因此可能会出现两种异常:
▲StackOverflowError
▲OutOfMemoryError
栈帧的内部结构:
1、局部变量表:主要用于存储方法参数和定义在方法体内的局部变量。
- 最基本的存储单元是Slot(变量槽),可以重用
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 Slot 上
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
2、操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
HotSpot JVM 设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
3、动态链接(指向运行时常量池的方法引用)
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。
- 在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 Class 文件的常量池中。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
4、方法返回地址:用来存放调用该方法的 PC 寄存器的值。
5、附加信息:例如,对程序调试提供支持的信息,但这些信息取决于具体的虚拟机实现。
三、本地方法栈(线程私有)
本地方法栈和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
在 HotSpot 虚 拟机中和 Java 虚拟机栈合二为一。 所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有 的。
和虚拟机栈一样,本地方法栈也可能会出现OOM和StackOverflowError这两种异常:
四、堆内存(线程共享)
对于大多数应用,Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存
为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能):
1、年轻代 (Young Generation):
年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为 Minor GC。年轻一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to 或 s0/s1),默认比例是8:1:1
2、老年代(Old Generation):
旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GC(Major GC),通常需要更长的时间。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝
3、元空间:
不管是 JDK8 之前的永久代,还是 JDK8 及以后的元空间,都可以看作是 Java 虚拟机规范中方法区的实现。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。所以元空间放在后边的方法区再说。
分代收集理论
依据分代假说理论,垃圾回收可以分为如下几类:
1、新生代收集(Minor GC/Young GC):目标为新生代的垃圾收集。
2、老年代收集(Major GC/Old GC):目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为。
3、混合收集(Mixed GC):目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。
4、整堆收集(Full GC):目标为整个堆和方法区的垃圾收集。
五、方法区(线程共享)
方法区(method area)只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码缓存等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而永久代(PermGen)是 Hotspot 虚拟机特有的概念, Java8 的时候又被元空间取代了,永久代和元空间都可以理解为方法区的落地实现。
永久代物理是堆的一部分,和新生代,老年代地址是连续的(受垃圾回收器管理),而元空间存在于本地内存(我们常说的堆外内存,不受垃圾回收器管理),这样就不受 JVM 限制了,也比较难发生OOM(都会有溢出异常)
在JDK1.7字符串常量池和静态变量被从方法区拿到了堆中,运行时常量池剩下的还在方法区, 也就是HotSpot中的永久代。
在JDK8 HotSpot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)
参考文章:
JVM 基础 - JVM 内存结构
JDK的运行时常量池、字符串常量池、静态常量池