JVM运行时内存区域结构
一、方法区(线程共享)
1. 什么是方法区?
方法区是系统分配的一个内存逻辑区域,用来存储类型信息(类的描述信息)。
2. 方法区的特点
- 方法区是线程安全的。由于方法区是线程共享的,所以方法区的数据访问必须设计成是线程安全的。例如,如果有多个线程访问方法区的同一个类,而这个类还没有被加载,这时候只能有一个线程去进行类加载,其他的线程必须等待。
- 方法区的大小不是固定的。JVM可以根据应用需要动态的调整。此外,方法区也不必是连续的。
- 方法区也可以被垃圾回收,当某个类不再被使用的时候,JVM将卸载这个类,进行垃圾回收。
3. 方法区里面存的是什么?
3.1 类型信息
对每个加载的类型,JVM必须在方法区保存这个类型的:
- 类型的全限定名
- 这个类型直接父类的完整有效名(java.lang.Object和Interface除外)
- 这个类型的修饰符(public,abstract, final的某个子集)
- 直接超接口的全限定名
- 类型标志(该类是类类型还是接口类型)
3.2 类型的常量池
存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象(在动态链接中起到核心作用)。
3.3 字段(域Filed)信息(该类声明的所有字段)
- 字段修饰符(public、protect、private、default)
- 字段的类型
- 字段名称
3.4 方法信息
方法信息中包含类的所有方法,每个方法包含以下信息:
- 方法修饰符
- 方法返回类型
- 方法名
- 方法参数个数、类型、顺序等
- 方法字节码
- 操作数栈和该方法在栈帧中的局部变量区大小
- 异常表
3.5 类变量(静态变量)
指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定。
3.6 指向类加载器的引用
每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。
3.7 指向Class实例的引用
类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。
3.8 方法表
为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的,类似C++虚函数表vtbl。
3.9 运行时常量池(Runtime Constant Pool)
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面常量和符号引用,这部分内容被类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个特征具有动态性,可以在运行期间将新的常量放入池中(典型的如String类的intern()方法)。
二、堆(线程共享)
1. 堆是什么?
堆是java中用来存放对象实例的一块内存,是JVM中最大的一块内存,在JVM启动时创建。
2. 堆的划分
java 中根据对象的存活周期不同将堆划为几个不同的区域,一般分为:(JVM内存的分代策略)
- 新生代
- 老年代
- (在HotSpot虚拟机中还存在永久代)
2.1 为什么要给堆分代?
堆是JVM管理的最大的一块内存区域,几乎所有的java对象都存在堆中。所以堆也是java中垃圾回收最活跃的区域。将堆分代主要是为了提高对象内存分配和垃圾回收的效率。
2.2 堆怎样提高内存分配和垃圾回收的效率?
在java中,将新创建的对象在堆的新生代中分配内存,经过多次垃圾回收依然存活下来的对象将会移放到老年代中。(在HotSpot虚拟机中,将静态属性、类信息等存放在永久代中。JDK8中将静态属性、类信息等存放在JVM 元空间中)新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率。
2.3 方法区和永久代的区别?
- 方法区(Method Area)是jvm规范里面的运行时数据区的一个组成部分,jvm规范中的运行时数据区还包含了:pc寄存器、虚拟机栈、堆、方法区、运行时常量池、本地方法栈。主要用来存储class、运行时常量池、字段、方法、代码、JIT代码等。运行时数据区跟内存不是一个概念,方法区是运行时数据区的一部分。方法区是jvm规范中的一部分,并不是实际的实现,切忌将规范跟实现混为一谈。
- 永久带又叫Perm区,只存在于hotspot jvm中,并且只存在于jdk7和之前的版本中,jdk8中已经彻底移除了永久带,jdk8中引入了一个新的内存区域叫metaspace。并不是所有的jvm中都有永久带,ibm的j9,oracle的JRocket都没有永久带,永久带是实现层面的东西,永久带里面存的东西基本上就是方法区规定的那些东西。
- 方法区是规范层面的东西,规定了这一个区域要存放哪些东西,永久带或者是metaspace是对方法区的不同实现,是实现层面的东西。
三、JVM栈 (线程私有,生命周期与线程的生命周期一致)
1.什么是JVM栈?
栈是JVM方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,栈帧中包括:局部变量表、操作数栈、方法返回地址等信息。每个方法从被调用到执行完成的这一过程对应的就是栈帧的入栈到出栈的过程。
2.栈帧中有什么?
2.1 局表变量表:方法定义的局部变量、方法的参数存在该表。
- 存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。
- 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间)
2.2 操作数栈:用来存放操作数。
局部变量表中的变量是不可直接使用的,如需使用必须通过相关指令将其加载至操作数栈中作为操作数使用。
2.3 方法返回地址
- 执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到的何种方法返回指令来决定,这种退出的方式称为正常完成出口。
- 方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出称为异常完成出口。
四、本地方法栈
本地方法栈和虚拟机栈的作用类似,区别仅仅是虚拟机栈为虚拟机执行的 Java 方法服务,而本地方法栈则是为 Native 方法服务。其具体实现由虚拟机自行规定。
五、程序计数器
是一块较小的内存区域,可以看做是当前线程执行的字节码的行号指示器。在虚拟机概念模型里,字节码解析器就是通过改变这个计数器的值来选取下一条需要执行的字节码,因此其在分支,循环,跳转,异常跳转,线程恢复等功能上都有着大作用。