我们从编写的程序入手,先了解Java程序有哪些东西需要在内存中存放,有了这一了解之后,我们再看Java内存区域的划分就容易一些。
一、我们编写的Java程序,JVM启动后,在内存中的类目清单
首先Java程序执行离不开class文件,我们所写的每一个类或者接口,编译之后都是.class文件。
在写一个java程序,我们通常所见的条目有类(或者接口)、变量、对象、方法。这些条目在内存中如何存放,是本篇文章想尝试说清的。
1、class文件包含的信息
包含类的版本等描述信息以及字段、方法、接口、常量池,具体内容如下ClassFile所示,对应jvms8
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count]; //类变量及实例变量
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
2、方法当中包含的内容
- 基本数据类型的局部变量(boolean, byte, char, short, int, float, long, double)
- 对象引用(深入理解Java虚拟机当中的描述是:reference类型,并不是对象本身,可能是指向对象起始地址的一个引用指针、也可能是一个指向代表对象的句柄或其它与此对象有关的位置)
- returnAddress类型(指向一个字节码指令的地址)
可以看到,方法中包含的大多为局部变量部分。这应该对后边所说的这部分在局部变量表中存储有帮助。我们知道每一个方法的调用和完成都对应着一个栈帧在虚拟机栈中的入栈和出栈过程。
3、对象实例
4、数组 等虚拟机规范中要求的变量
基于以上Java程序在内存当中的类目(当然可能并不全),我们来看Java内存区域
二、Java 运行时数据区
1、程序计数器
功能:程序计数器用来保存当前线程下一条要执行的字节码指令地址。
说明:因为JVM支持多线程,所以程序计数器为线程私有。也就是说,每个线程都有一个程序计数器。这个很好理解,例如在单核CPU上多线程并发执行,如果没有线程自己私有的指令计数器,那线程怎么知道自己执行到哪里了?如何恢复执行?
如果当前执行的方法不为native方法,那么pc中存放下一条要执行的字节码指令的地址。如果当前执行的方法是native方法,那pc种的值为undefined。
2、Java虚拟机栈
功能:用于栈帧存储,栈帧用于方法的执行,保存局部变量和部分结果。每一个方法在调用时就伴随这一个栈帧的创建,方法完成时就伴随着栈帧的销毁。
说明:栈帧的具体功能在虚拟机规范中有介绍,这一功能对于大多语言基本相同。虚拟机规范中的描述是:栈帧用于存储数据和部分结果,用来执行动态链接、为方法返回值、进行异常分发。每个栈帧都有自己的局部变量表、操作数栈、和一个指向当前方法所在类的运行时常量池的引用。
局部变量表:一个局部变量空间可以存储boolean, byte, char, short, int, float, reference(引用地址), or returnAddress(字节码指令地址),64位的long和double需要两个局部变量空间来存储。
方法调用:在方法调用时,虚拟机使用局部变量表来传递参数。局部变量0总是用来传递被调用方法所在的实例对象的引用。方法的参数从局部变量1开始、连续排列。
操作数栈:每个frame都包含一个操作数栈,其栈深在编译期即完全确定
作用一:如其字面意思,JVM指令先将局部变量或者field(如前ClassFile中所言)中的值或者常量加载到操作数栈中,然后JVM指令从操作数栈提取值并进行操作,然后将结果再压入操作数栈。
作用二:用于准备传递给方法的参数、并接收方法的返回值。
动态链接:前边说到每个frame都有一个指向当前方法所在类的run-time constant pool(运行时常量池)的引用,这正是动态链接的关键所在。首先要明确的是,动态链接的是啥?虚拟机规范中所说,通过动态链接,要被调用的方法被转化成方法的实际引用,要被访问的变量被放入这些变量在运行时数据区的存储结构的合适位置。当运行时常量池中找不到这些符号时,也就是说此时这些符号处于as-yet-undefined(当前未定义状态),要进行类加载。
3、堆
功能:java虚拟机规范中描述的是所有的对象实例和数组都在堆上分配存储空间。堆由线程共享。
说明:逃逸分析技术使得上述结论也并不绝对了,也就是说方法体内new的对象,如果指针未发生逃逸(没有被其它方法或者线程引用)那就直接在栈上分配。这带来的好处是减少的内存回收的压力。
4、方法区
功能:存储每个被虚拟机加载的类的组成结构信息,如上ClassFile描述的那样,类的描述信息、常量、静态变量、即时编译后的代码等。
虚拟机中的描述是,Java的方法区类似常规语言的编译代码的存储区域。它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法。
5、运行时常量池
功能:存放编译期生成的各种字面量以及需要在运行时解决的符号引用。
说明:每个class都有一个运行时常量池,组成constant_pool table(常量池表),在类或者接口被加载的时候就会构建常量池。在前面的ClassFile中可见constant_pool信息,而方法区存放着类的组成结构信息,据此也可以判定运行时常量池是方法区的一部分。
6、本地方法栈
功能:执行native方法使用。由线程私有。
说明:本地方法栈和虚拟机栈的作用非常相似,只不过虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务。
最后:对各类目在内存中怎么存放有了解之后,对内存回收以及OOM的理解也有很大帮助,后边的文章继续深入展开,恳求看到的小伙伴多提意见,让我好发现错误及时纠正。