一:JVM内存模型
首先看两个模型图:
图1.
图2.
上面两个图都展示了虚拟机内存管理模式,整个运行时数据区又分为不同的内存区域,不同区域承担不同的功能,下面我们来一一分析一下它们的作用:
1.程序计数器(PC Register)
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程职工的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,并且不能互相被干扰,我们称这类内存区域为线程私有内存。
2.java虚拟机栈(JVM Stack)
java虚拟机栈也是线程私有的,虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress(指向了一条字节码指令的地址)。
3.本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈所发挥的作用非常相似,其区别不过是虚拟机栈为虚拟机指向java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
4.java堆(Heap)
对于大多数应用来说,java堆(Heap)是java虚拟机所管理的内存中最大的一块。java堆被所有线程共享,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例和数组,是垃圾收集管理器的主要区域。根据java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
5.方法区(Method Area)
方法区也称“永久代”,它用于存储虚拟机加载的类信息、常量、静态变量,是各个线程共享的内存区域。
运行时常量池(Runtime Constant Pool):是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
从JDK7开始移除永久代(但并没有移除,还是存在),存储在永久代的一部分数据已经转移到了Java Heap或者是Native Heap。符号引用转移到了native heap;字面量转移到了java heap;类的静态变量转移到了java heap。
6.直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。
二:实例分析
逻辑内存模型我们已经看到了,那当我们建立一个对象的时候是怎么进行访问的呢?在java语言中,对象访问是如何进行的?对象访问在Java语言中无处不在,是最普通的程序行为,这都会涉及到java栈、java堆、方法区这个三个重要的内存区域之间的关联关系,如下面代码:
public class JVMMemoryTest {
/**
* jvm自动寻找main方法,执行main方法,此时虚拟机栈中有一个代表main方法的栈帧入栈,执行完毕后出栈
* @param args
*/
public static void main(String[] args) {
/**
* 1.student是对象的引用,所有会保存在栈帧的局部变量里
* 2.创建Student的时候,首先进行类的加载工作,类只会加载一次,将类的类型信息数据加载到jvm的方法区中,如果之前加载过了,那么就不会再加载。
* 3.类的加载其实就是将.class文件加载进虚拟机内存中,在加载的时候,在java堆中生成对应的Class对象,
* 最后生成一个Student对象在堆中
*/
Student student=new Student(18,"tom","007");
/**
* 声明定义一个int类型的变量a,因为a是基本数据类型,所以在栈中直接分配一个内存保存这个变量
*/
int a=9;
int b=10;
/**
* 执行study方法,在栈中加入一个栈帧,执行完毕后这个栈帧将出栈
* 在study方法中,有两个int类型的局部变量,是保存在栈帧的局部变量内存区中的
*/
student.study(a,b);
}
//静态内部类
public static class Student extends Person implements IStudyable {
private static int cnt = 5;
static {
cnt++;
}
private String sid;
public Student(int age, String name, String sid) {
super(age, name);
this.sid = sid;
}
public void run() {
System.out.println("run()...");
}
public int study(int a, int b) {
int c = 10;
int d = 20;
return a + b * c - d;
}
public static int getCnt() {
return cnt;
}
}
//父类
static class Person {
private String name;
private int age;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public void run() {
}
}
//接口
interface IStudyable {
public int study(int a, int b);
}
}
上面例子展示了jvm是如何分配内存的,还有需要说明的就是new Student的时候,在java堆中形成一块存储了Student类型的所有实例数值(instance Data,对象中的各个实例字段数据)的结构化内存,这块内存的长度是不固定的。另外,在java堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据是存储在方法区中的。
不同的虚拟机实现对象的访问方式有所不同,主流的方式有两种:使用句柄和直接指针。
如果使用句柄访问方式,java堆中将会划出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如图所示:
如果使用指针直接访问的方式,java对对象的布局中就必须放置有访问类型数据的指针,而reference中直接存储的是对象的地址,如图所示:
这两种访问方式各有优势,其中使用直接指针访问方式最大的好处就是访问速度快,可节省程序的执行成本。