谈谈JVM内存区域的划分

对 Java 程序员来说我们不用自己手动管理对象内存的申请与释放,全部交由 Java 虚拟机(JVM)来管理内存的分配与回收。
因此,日常开发中我们不用关心内存分配与回收,减少了很多繁琐的工作,大大提高了开发效率。
也正是因为如此,一旦出内存泄漏和溢出方面的问题,如果不了解 JVM 内部的内存结构、工作机制,那么排查问题将变得异常艰难。

接下来,我们一起学习 JVM 内存区域的划分、作用以及可能产生的问题。

根据 Java 虚拟机规范,Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为几个不同的数据区域,如下所示:

其中,有些区域会在虚拟机进程启动的时候创建,由所有线程共享。还有些区域则在用户线程的启动的时候创建,线程结束的时候销毁。
这部分区域则是线程私有的。JVM 内存区域主要分为线程共享区域(Java堆、方法区)、线程私有区域(程序计数器、虚拟机栈、本地方法栈)。

1、程序计数器(Program Counter Register)
程序计数器也叫PC寄存器。是一块较小的内存空间。
在 JVM 规范中,每个线程都有自己的程序计数器,独立存储,互不影响,也就是说程序计数器是线程私有的。
如果当前线程执行的是一个 Java 方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是本地(Native)方法,则是空(Undefined)。
此内存区域是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域。

2、Java 虚拟机栈(VM Stack)
Java 虚拟机栈也是线程私有的,每个线程在创建时都会创建一个虚拟机栈,生命周期与线程相同。
虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)压入虚拟机栈,方法执行完毕栈帧出栈。
栈帧中存储着局部变量表、操作数栈、动态链接、方法出口等信息。
在 JVM 规范中对虚拟机栈规定了两类异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;
如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。
可以通过参数-Xss 设定Java虚拟机栈空间大小。

3、本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈类似,也是每个线程都会创建一个。区别是,虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
JVM 规范中并未对本地方法栈的实现做强制规定,具体虚拟机可以根据需要自由实现它。甚至在 Oracle Hotspot JVM 中将本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常。

4、堆(Heap)
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。它是Java内存管理的核心区域,用来存放 Java 对象实例,几乎所有的 Java 对象实例都被直接分配在堆上。
但是随着即时编译技术的进步和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化手段将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
从 JDK1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆。从垃圾回收的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,
所以 Java堆还可以细分为:新生代(Eden区、From Survivor区 和 To Survivor区)和老年代。
无论如何划分,Java 中存储的都是对象的实例,这一点上是不变的,而将 Java堆细分的目的只是为了更好的回收内存,或者更快的分配内存。

如果在Java堆中没有足够的内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将抛出 OutOfMemoryError 异常。
通过参数-Xms 和 -Xmx 设定初始堆大小和最大堆大小。

5、方法区(Method Area)
方法区也是所有线程共享的一块内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
由于早期 HotSpot JVM 使用永久代实现方法区,很多人习惯将方法区称为永久代(Permanent Generation)。
方法区是Java虚拟机规范中的定义,是一种规范,而永久代则是一种是实现,一个是标准一个是实现,
其他的虚拟机(比如 BEA JRockit、IBM J9等)实现并没有永久代这一说法。

方法区的发展
由于永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、类型的卸载)的效果比较难以令人满意,
我们通常使用 -XX:PermSize 和 -XX:MaxPermSize 设置永久代的大小, 32位机器默认的永久代大小为64M,64位的机器则为85M。
一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误(OOM)。

在 JDK6 的时候 HotSpot 团队就有放弃永久代逐步改为本地内存(Native Memory)来实现方法区的计划了,
到了 JDK7 已经把原本放在永久代的字符串常量池、静态变量等移到堆上分配,
在 JDK8 中彻底移除了永久代,将 JDK7 中永久代剩余的内容(主要是类的元数据)移到元空间(Metaspace)中。
移除永久代是为融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,不需要配置永久代。

元空间(Metaspace)
元空间与永久代最大的区别在于:元空间并不在 Java虚拟机中,而是使用本地内存(Native Memory)。
因此,默认情况下,元空间大小仅受本地内存限制。
类的元数据放入本地内存,字符串常量池和类的静态变量放入 Java堆中,这样加载多少类的元数据就不再由 MaxPermSize 控制,
而由系统的实际可用空间来控制。
通过参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 设定元空间初始值和最大值。

相关文章

坚持原创技术分享,您的支持将鼓励我继续创作!