JVM - 内存结构简介
# JVM - 内存结构简介
运行一个 Java 应用程序,必须要先安装 JDK 或者 JRE 包。因为 Java 应用在编译后会变成字节码,通过字节码运行在 JVM 中,而 JVM 是 JRE 的核心组成部分。JVM 不仅承担了 Java 字节码的分析和执行,同时也内置了自动内存分配管理机制。这个机制可以大大降低手动分配回收机制可能带来的内存泄露和内存溢出风险,使 Java 开发人员不需要关注每个对象的内存分配以及回收,从而更专注于业务本身。
在 Java 中,JVM 内存模型主要分为堆、方法区、程序计数器、虚拟机栈和本地方法栈。其中,堆和方法区被所有线程共享,虚拟机栈、本地方法栈、程序计数器是线程私有的。
# 堆
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden
和 Survivor
区,最后 Survivor
由 From Survivor
和 To Survivor
组成。
但需要注意的是,这些区域的划分因不同的垃圾收集器而不同。大部分垃圾收集器都是基于分代收集理论设计的,就会采用这种分代模型。而一些新的垃圾收集器不采用分代设计,比如G1 收集器
就是把堆内存拆分为多个大小相等的 Region
。
# 方法区
在 jdk8 之前,HotSopt 虚拟机的方法区又被称为永久代,由于永久代的设计容易导致内存溢出等问题,jdk8 之后就没有永久代了,取而代之的是元空间(MetaSpace)。元空间并没有处于堆内存上,而是直接占用的本地内存,因此元空间的最大大小受本地内存限制。
方法区与堆空间类似,是所有线程共享的。方法区主要是用来存放已被虚拟机加载的类型信息、常量、静态变量等数据。方法区是一个逻辑分区,包含元空间、运行时常量池、字符串常量池,元空间物理上使用的本地内存,运行时常量池和字符串常量池是在堆中开辟的一块特殊内存区域。这样做的好处之一是可以避免运行时动态生成的常量的复制迁移,可以直接使用堆中的引用。要注意的是,字符串常量池在 jvm 中只有一个,而运行时常量池是和类型数据绑定的,每个 Class 一个。
1、类型信息(类或接口)
- 这个类型的全限定名
- 这个类型的直接超类的全限定名(只有
java.lang.Object
没有超类) - 这个类型的访问修饰符(public、abstract、final)
- 这个类型是接口类型还是类类型
- 任何直接超接口的的全限定名的有序列表
2、运行时常量池
- Class 文件被装载进虚拟机后,Class 常量池表中的字面量和符号引用都会存放到运行时常量池中,平时我们说的常量池一般指运行时常量池。
- 运行时常量池相比Class常量池具备动态性,运行时可以将新的常量放入池中,比如调用
String.intern()
方法使字符串驻留。
3、字段信息
- 字段名
- 字段的类型(包括 void)
- 字段的修饰符(public、private、protected、static、final、volatile、transient)
4、方法信息
- 方法名
- 方法的返回类型
- 方法参数的数量和类型
- 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract)
- 方法的字节码
- 操作数栈和该方法的栈帧中的局部变量的大小
- 异常表
5、指向类加载器的引用
- jvm 使用类加载器来加载一个类,这个类加载器是和这个类型绑定的,因此会在类型信息中存储这个类加载器的引用
6、指向 Class 类的引用
- 每一个被加载的类型,jvm 都会在堆中创建一个
java.lang.Class
的实例,类型信息中会存储 Class 实例的引用 - 在代码中,可以使用 Class 实例访问方法区保存的信息,如类加载器、类名、接口等
# 虚拟机栈
每当启动一个新的线程,虚拟机都会在虚拟机栈里为它分配一个线程栈,线程栈与线程同生共死。线程栈以 栈帧 为单位保存线程的运行状态,虚拟机只会对线程栈执行两种操作:以栈帧为单位的压栈或出栈。每个方法在执行的同时都会创建一个栈帧,每个方法从调用开始到结束,就对应着一个栈帧在线程栈中压栈和出栈的过程。方法可以通过两种方式结束,一种通过 return 正常返回,一种通过抛出异常而终止。方法返回后,虚拟机都会弹出当前栈帧然后释放掉。
当虚拟机调用一个Java方法时.它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入Java栈中。
栈帧由三部分组成:局部变量区、操作数栈、帧数据区。
1、局部变量区
- 局部变量区是一个数组结构,主要存放对应方法的参数和局部变量。
- 如果是实例方法,局部变量表第一个参数是一个 reference 引用类型,存放的是当前对象本身 this。
2、操作数栈
- 操作数栈也是一个数组结构,但并不是通过索引来访问的,而是栈的压栈和出栈操作。
- 操作数栈是虚拟机的工作区,大多数指令都要从这里弹出数据、执行运算、然后把结果压回操作数栈。
3、帧数据区:主要保存常量池入口、异常表、正常方法返回的信息
常量池入口引用
:某些指令要从常量池取数据,获取类、字段信息等异常表引用
:当方法抛出异常时,虚拟机根据异常表来决定如何处理。如果在异常表找到了匹配的 catch 子句,就会把控制权转交给 catch 子句的代码。没有则立即异常中止,然后恢复发起调用的方法的栈帧,然后在发起调用的方法的上下文中重新抛出同样的异常。方法返回信息
:方法正常返回时,虚拟机通过这些信息恢复发起调用的方法的栈帧,设置PC寄存器指向发起调用的方法。方法如果有返回值,还会把返回结果压入到发起调用的方法的操作数栈。
# 本地方法栈
本地方法栈与虚拟机栈所发挥的作用是相似的,当线程调用Java方法时,会创建一个栈帧并压入虚拟机栈;而调用本地方法时,虚拟机会保持栈不变,不会压入新的栈帧,虚拟机只是简单的动态链接并直接调用指定的本地方法,使用的是某种本地方法栈。比如某个虚拟机实现的本地方法接口是使用C连接模型,那么它的本地方法栈就是C栈。
本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,它可以做任何他想做的事情,本地方法不受虚拟机控制。
# 程序计数器
每一个运行的线程都会有它的程序计数器(PC寄存器),与线程的生命周期一样。执行某个方法时,PC寄存器的内容总是下一条将被执行的地址,这个地址可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时PC寄存器的值是 undefined
。
程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。多线程环境下,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
# 文章来源
作者:bojiangzhou 链接:https://juejin.cn/post/6917256143160999950 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。