JVM第一篇—内存区域

Meteor 2023-10-11 15:24:15
Categories: Tags:

简介

JVM(Java Virtual Machine)Java虚拟机,实现Java代码 write onece, run everywhere 的关键,主要用于优化、运行字节码文件,当然,对于Java虚拟机而言,运行的文件就是Java类

对于Java而言,由于将垃圾回收交给了JVM处理,自己不用手动清理垃圾,因此如果发生了内存泄露的问题,不了解JVM将会很难排查出问题,所以JVM的一大核心就是对内存的管理,本篇将介绍JVM如何进行内存区域划分,了解在Java代码运行时如何存储数据的

以下所有关于JVM的介绍都是基于 HotSpot 虚拟机

以下是 JVM 运行时数据区域在jdk8以前和之后的发展变化,其实表面上看只是将方法区替换成了元空间,但实际上还有更细节的变化,接下来会慢慢讲述

方法区

所有线程共享区域,用于存储Java类的元信息,包括类型信息、常量、静态变量等等,也就是一些静态文件以及运行时生成的代理信息

在jdk8之前,由于使用HotSpot虚拟机的较多,而HotSpot中用来实现方法区的区域叫永久代,因此在上图中,直接使用了永久代的称呼,而jdk8开始,由于HotSpot和JRockit合并,在JRockit中并没有永久代,而且HotSpot开发者本就打算废弃永久代,终于在jdk8时采用元空间实现方法区

永久代和元空间两种实现方式的区别在于,前者是在堆中分配的,内存是固定且有上限的,无法动态扩展,后者使用本地内存,提高了内存的上限,且支持动态分配和释放内存,因此元空间可以完全取代永久代

在方法区进行的垃圾收集行为很少,因为针对它的回收目标主要是常量池的回收和对类型的卸载,回收效率不高,尤其是对类型的卸载,需要满足下面三个条件才会考虑回收:

所有线程共享区域,几乎所有对象实例都在这上面分配内存,也是垃圾回收的主要区域。由于大部分垃圾收集器是基于分代理论设计的,因此划分堆中区域时会有“新生代”、“老年代”、“永久代”的名词,但这些名词只是针对那些基于分代设计的收集器,目前已经有很优秀的收集器不基于分代理论,比如G1

堆在内存上的存储可以连续也可以不连续,但在逻辑上应该是连续的。其大小可以固定也可以扩展,目前主要的虚拟机都是可扩展的,如果堆已经占满而且无法扩展时就会报错:OutOfMemoryError

程序计数器

由于一般线程数多于处理器数量,因此线程之间的切换较为频繁,每个线程切换前需要先保存自己的信息,在执行Java方法时程序计数器记录的是虚拟机字节码指令的地址,如果执行本地方法,计数器的值为空

虚拟机栈

线程私有区域,它存储的是包含局部变量表、操作数栈、动态连接、方法出口等信息的栈帧,而每个栈帧的入栈、出栈代表着一个方法从调用到执行完毕的过程,因此每个栈帧的开始都是 main 方法

局部变量表

在编译阶段就已经确定了局部变量表的大小,它使用变量槽的方式存储局部变量,除了long和double类型的变量占用两个变量槽,其它类型的变量均占用一个变量槽,而变量槽的大小没有明确的规定,不过对于非 static 方法,第0号变量槽存储的是实例变量本身,即 this

操作数栈

存储方法执行过程中产生的中间结果,因此一开始是空的,和局部变量表一样,除了long和double类型的变量占用两个单位的容量,其它占用一个单位

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。因为Java代码在进行Javac编译的时候并不会进行连接操作,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中

方法出口

方法返回时,可能需要在栈帧中存储一些信息来帮助调用者恢复状态,比如说方法正常退出时会存储调用者的程序计数器的值,但如果出现异常,返回地址就需要异常处理器来确定。另外,如果有返回值,也需要压入调用者的操作数栈中,最后还要调整程序计数器的值指向下一条命令

本地方法栈

线程私有区域,作用和虚拟机栈类似,区别在于虚拟机栈是为虚拟机执行Java方法提供支持,本地方法栈则是为执行本地方法提供服务。

Java中的本地方法随处可见,在Object类中就有很多标注了native的方法,比如clone()、getClass()、hashCode()等等,使用本地方法的原因在于Java的局限性,它需要依赖于底层操作系统的支持才能完成一些功能,或者为了加速一些代码的执行效率而采用C/C++的实现方式