jvm将内存分为方法区、堆区、栈区和本地方法栈。其中堆区一般最大,对于部分jvm实现来说,方法区和栈区有可能是从堆顶分配而来的空间。本地方法栈是不受虚拟机限制的内存区域,由具体实现语言(C或者C++)来进行管理。其中方法区用来装载类信息和常量信息;堆区用来保存对象数据;栈区保存运行时数据;本地方法栈用来保存本地方法运行时数据。如下图所示:
方法区中装载的每个类包含:
1.类型信息:
- 类的全限定名
- 类的直接超类的全限定名
- 类的类型(接口、类)
- 类修饰符
- 类的常量池 (ps:方法区为每一个被装载的类维护了一个常量池)
- 类的字段信息
- 字段修饰符
- 字段类型
- 字段名
- 方法信息
- 方法修饰符
- 返回类型
- 方法名
- 方法字节码
- 操作数栈和该方法的的栈帧中的局部变量区的大小
- 方法的字节码
- 异常表
- 静态变量 (编译时常量处理方式有所不同)
- 它所属的classloader的引用
- 指向它的class对象的引用
每个被装载的类在装载之后都会生成一个java.lang.Class对象,这个引用就是指向了它。
2.方法表
为了加快访问速度,虚拟机在装载类时,会为每一个非抽象类和非接口类实现一个方法表,包括父类或者超类继承来的方法。数据结构因实现者不同而不同,一般都是一个数组,运行时调用方法可以直接访问方法表快速定位方法位置。
3.类的字节码
运行class文件当然要有字节码,不然虚拟机跑个pi呀
堆区
所有创建的引用类型对象都存放在了堆区,java程序猿只负责实例化堆区对象,由垃圾回收器负责回收不可用的对象。
对象在堆区的表示或者数据结构有虚拟机实现者来定义,存在差距。
堆空间中对象必须要有一个指向方法区中类信息的指针,可能的数据结构如下图1:句柄池存放着所有堆中对象的句柄,每个句柄对象包含两个指针,一个指向堆中的实例对象,另一个指向方法区中的类信息,在函数栈或者其他数据结构中存储句柄池中句柄的引用(即对象的引用)。还有一种设计就是将句柄池和对象池合并,在此不再赘述,见下图2.
图 1. 堆区实现方式一 图 2. 堆区实现方式二
程序计数器
对于每一个线程来说,都有自己的pc寄存器(程序计数器),它在线程启动时被创建,大小为一个字长。总是指向下一个要执行的指令的地址。如果下一个指令是调用一个本地方法,那pc值为“undefined”
java栈
对于每一个线程来说,都有字节的java栈。栈以栈帧为单位保存当前线程的运行状态,且只有两种操作,入栈和出栈。当前栈帧对应当前方法,每一个方法只有一个栈帧,方法内的局部变量、参数、中间运算结果均保存在同一个栈帧中。当方法结束时(正常return或者抛出异常),该方法对应的栈帧都会被出栈。
栈帧。java栈的单位,由三部分组成:
- 局部变量表
以字长为单位,在编译时确定大小。一般用数组表示,下表从0开始,如果是静态方法,则变量表0位置为函数参数第一个数据;如果是实例方法,则变量表0位置 是该方法所属对象的this指针,类型为reference。对于局部变量中对象,存储类型为reference(引用),即对象的指针。并且byte、short、boolean、char类型在局部变 量表里面均是以int存储,等方法执行结束,除boolean外,其他类型都将被还原为原类型。
- 操作数栈
- 和局部变量相同,以字长为单位,在编译时确定大小。只有出栈和入栈两种操作
- 帧数据区
- 动态大小。包含常量池信息、方法正常返回、异常表和调试信息。
本地方法栈
本地方法栈是虚拟机或者java程序调用本地方法时产生的本地方法栈。
当一个java线程调用本地方法时不会产生新的栈帧,然后本地方法开始执行,本地方法执行完成时,返回数据给java线程的当前栈帧。并且本地方法一般是由C或者C++编写,其执行权限和java虚拟机相同,可以获取系统任意操作。
eg:
java当前线程栈调用了两个java方法产生了两个java栈帧,然后第二个java方法调用了本地方法,产生了本地方法栈,由于本地方法可以通过本地方法接口调用任意java函数,所以本地方法调用了新的java函数,产生了两个新的栈帧。