Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为多个区域,这些区域各有自己的用途以及独特的创建和销毁时间,今天就来揭开这些不同的数据区域的神秘面纱
先来一张最经典的图:
程序计数器
程序计数器里记录的是当前线程字节码指令执行到的位置。
程序计数器的生命周期是随着一条线程的启动而创建的,每一个线程独有一个程序计数器,多个线程之间互不影响。(可以理解为Java中的ThreadLocal,相关文章可参考:ThreadLocal及InheritableThreadLocal的原理剖析)
程序计数器为什么要这样设计呢?
我们知道多线程其实就是通过线程轮流切换并分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。当切换到另外一条线程时,若是当前线程没有程序计数器来记录此刻的执行位置,下次处理机再执行这条线程时就不知道该从哪开始了。
栈
本地方法栈和虚拟机栈可以统称为栈,由于本地方法栈是jvm调用操作系统native方法所使用的栈且它们的作用是非常相似的,所以这里重点看一下虚拟机栈。
虚拟机栈与程序计数器一样,也是线程私有的,每个线程都会有一个自己的虚拟机栈。它描述的java方法执行的内存模型
为什么是Java方法执行的内存模型呢?
在虚拟机中,每一次方法调用都会创建栈帧,这个栈帧的生命周期就伴随着这个方法的执行周期。一个栈帧的组成主要包含以下部分:
局部变量表、操作数栈、常量池指针、动态地址、方法返回地址等信息。
局部变量表
存储方法中声明的非静态变量以及方法形参
其中基本变量直接存储值
引用类型存储指向对象的引用
它的大小在编译期就确定了,程序执行期间不会发生改变
操作数栈
Java中所有的参数传递都是依靠操作数栈进行的,例如如下代码:
static int methed1(int a,int b){
int c=0;
c=a+b;
return c;
}
其实这短短的三行代码执行的过程是这样的:
1. 0压栈
2. 弹出int存放局部变量c
3. 局部变量a压栈
4. 局部变量b压栈
5. 弹出两个变量求和,将结果压栈
6. 弹出结果放到局部变量c
7. 局部变量c压栈
8. return
常量池指针
因为方法中有可能使用类中的常量,所以必须有指向常量池的指针。
动态链接
在虚拟机运行的时候,运行时常量池会保存每个方法的间接引用,如果栈帧A的方法想调用栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法,这时候就有一点需要注意,如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。
方法返回地址
方法的返回分为两种情况
正常情况,方法正常执行完毕退出后会根据方法是否定义返回值来决定是否要传返回值给上层的调用者
执行过程中出现异常,异常导致的方法结束不会传返回值给上层的调者
两种方法返回在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,如果是因为异常退出的,则是需要通过异常处理表来确定
方法的的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操作,因此方法退出时可能做的事情包括:恢复上层方法的局部变量表以及操作数栈,如果有返回值的话,就把返回值压入到调用者栈帧的操作数栈中,还会把PC计数器的值调整为方法调用入口的下一条指令
堆
在我们的程序中,跟我们打交道最多的就是堆里的对象了。基本上所有(不包括常量池中存在的)通过new操作创建的对象都会保存在堆中。所以与栈的线程私有不同,堆是所有线程共享的(毕竟不共享难道每个线程调用时都new一次对象岂不是疯了),所以它也是虚拟里最大的一块。关于堆的更多内容请持续关注博客更新
方法区
方法区同样是各个线程共享的内存区域,它主要存储已经被虚拟机加载的类信息
类信息
类的全限定名
父类的全限定名
直接实现接口的全限定名
类型标志
类的访问描述符(public、private、default、abstract、final、static)
常量池
存放该类所用到的常量的有序集合
字段信息
字段修饰符(public、protect、private、default)
字段的类型
字段名称
类的所有方法信息
方法修饰符
方法返回类型
方法名
方法参数个数、类型、顺序等
方法字节码
操作数栈和该方法在栈帧中的局部变量区大小
异常表
类静态变量
指向类加载器的引用
指向Class实例的引用
可以通过Class.forName获取的引用
方法表
非抽象类、非接口的类才会有,一个保存类中所有的方法的数组,数组中每个每个元素是对每个方法的直接引用
运行时常量池
当类和接口被加载到JVM后,对应的运行时常量池就被创建出来了,与常量池的不可变不同,运行时常量池是可变的,比如String的intern方法就可以做到
综合复习
public class User{
private String name;
public User(String name){
this.name = name;
}
//省略getset方法
}
public class Test{
public static void main(String[] args){
User user1=new User("张三");
User user2=new User("李四");
}
}
分析上方的一段代码,即可得到下方的图
万水千山总是情,点个 “在看” 行不行!!!
