理解JVM的GC机制之JVM内存划分
前言
什么是JVM的GC,GC(Garbage Collection)也就是垃圾回收是JVM在内存空间不足等必要条件下自动清除应用程序中的垃圾对象,回收垃圾对象占用的内存空间。
JVM的GC机制使得开发者从繁琐的内存管理中释放出来,只需要专注于业务开发。但即便如此,这并不意味着我们可以不用去理解GC的原理,如果不了解GC原理的话,那么当我们在实际工作中遇到了内存泄漏、频繁GC导致应用性能低下的问题时往往会变得束手无措,为了能够快速排查问题、解决性能瓶颈,我们需要对GC原理有所了解。
想要了解GC原理,我们需要先了解JVM内存管理机制,了解JVM中内存的划分,这样才能更好的了解垃圾回收是回收的是哪些区域的垃圾对象,什么时候回收以及怎么回收。
JVM的内存管理
根据JVM规范(这里及下文的规范均为Java8的规范),JVM把内存划分成了几个不同的区域:
- 程序计数器(线程独享)
- 方法区(线程共享)
- 堆区(线程共享)
- 虚拟机栈(线程独享)
- 本地方法栈(线程独享)
其中,方法区和堆区为所有线程共用的数据区域,程序计数器、虚拟机栈、本地方法栈为各个线程独有的区域,不同的区域作用不尽相同。
对于线程独享的区域,区域创建于线程创建之时,销毁于线程退出之时。对于线程共享的区域,区域创建于虚拟机启动之时,销毁于虚拟机退出之时。
# 程序计数器
程序计数器为 线程独享 区域,是当前线程所执行的字节码的行号指示器,记录当前线程执行到哪里了。字节码解释器就是通过改变计数器的值来选去程序接下来执行的操作。
由于程序计数器仅仅是记录当前线程所执行的指令地址,所以不存在内存溢出的情况,该区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError
情况的区域。
# 虚拟机栈
与程序计数器一样,虚拟机栈也是 线程独享 的,在线程创建时虚拟机栈就会被创建,所以我们一般说的Java栈
就是指虚拟机栈。虚拟机栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表、操作数栈、方法返回地址等信息。
局部变量表(比较特别):存储方法中的局部变量,包括方法中声明的非静态变量以及函数形参。对于基本类型类型直接存储变量的值,对于引用类型的变量,则存储变量指向对象的引用地址。因此局部变量表的大小在编译阶段就可以确定内存大小,运行时不会改变。
当方法被线程调用时,栈帧被创建入栈,当方法调用结束时,栈帧出栈。所以,线程当前执行方法对应的栈帧必定位于虚拟机栈的顶部,并且由于每个线程正在执行的方法可能都不会相同,因此虚拟机栈是线程私有的,互不干扰。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则会抛出StackOverflowError
;不过大多数虚拟机都允许动态拓展虚拟机栈的大小,所以线程可以一直申请栈,直到内存不足,此时会抛出OutOfMemoryError
。
# 本地方法栈
本地方法栈与虚拟机栈的作用与工作原理类似,也是 线程独享,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机执行Native方法服务。在JVM规范中,并没有对本地方法栈做出强制规定,虚拟机可以自由实现它,在HotSopt虚拟机中直接把本地方法栈与虚拟机栈合并在一起。
本地方法栈与虚拟机栈类似,也可能会抛出StackOverflowError
和OutOfMemoryError
异常。
# 堆
堆是虚拟机中最主要的内存区域,也是GC机制中最重要的区域,接下来会详细介绍堆区内的划分。
他被所有 线程共享 ,在虚拟机启动时创建,几乎所有的对象实例及数组都存储在堆中,所以堆区是JVM所管理的内存区域中最大的一块区域。
根据JVM规范的规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小,也可以是可拓展的,目前主流的虚拟机都是可拓展的。
如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再拓展,堆区将会抛出OutOfMemoryError
异常。
#方法区
方法区与堆区一样,是被所有 线程共享 的,在虚拟机启动时创建。
在方法区中,主要存储的是已经被虚拟机加载的类信息(如字段属性、类方法、构造函数等)、final常量、静态变量等。
运行时常量池:是方法区的一部分,用于存储编译期间就生成的字面常量、符号引用、直接引用;
方法区在物理上是不需要连续的,且在大小上可以是固定大小也可以是可拓展大小的,除此之外,JVM规范中也明确说明此区域是可以不进行垃圾回收。
当类或方法被虚拟机加载但方法区的内存不足以继续分配内存时,会抛出OutOfMemoryError
异常。
更多的信息可以看看JVM规范中相关的内容。