按照Java虚拟机规范的规定,JVM自动管理的内存将包括以下几个运行时的数据区域:
下面分别对几个数据区域进行说明:
1.程序计数器
程序计数器是JVM中一块较小的内存区域,保存着当前线程执行的虚拟机字节码指令的内存地址.
Java多线程的实现,其实是通过线程间的轮流切换并分配处理器执行时间的方式实现的,在任何时刻,处理器都只会执行一个线程的指令. 在多线程场景下,为了保证线程切换回来后,还能恢复到原来的状态,找到原先执行的指令,所以每个线程都会设立一个程序计数器,并且各个线程之间互不影响,程序计数器为"线程私有"的内存区域.
若当前线程正在执行的是Java方法,则程序计数器保存的是虚拟机字节码的内存地址,若正在执行的Native方法(非Java方法),则程序计数器为空.
程序计数器是唯一一个在Java规范中没有规定任何OutOfMemory(内存耗尽)场景的区域
2.虚拟机栈
虚拟机栈和线程是紧密联系的,每创建一个线程就会对应创建一个Java栈,生命周期和线程相同,所有虚拟机栈也是"线程私有"的内存区域.
这个栈中对应多个栈帧,每调用一个方法就会往栈中创建并压入一个栈帧,栈帧是用来存储方法数据和部分过程结果的数据结构,每一个方法从调用到最终返回结果的过程,就对应一个栈帧从入栈到出栈的过程.
线程运行过程中,只有一个栈帧处于活跃状态,被称为"当前活动帧栈",当前活动帧栈始终是虚拟机栈的栈顶元素.
在Java虚拟机规范中,对这个区域规定了两种异常情况:
- 若线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归)
- 虚拟机栈可以动态扩展,若扩展到无法申请足够的内存空间,就会出现OOM
3.本地方法栈
本地方法栈和虚拟机栈的作用相似,只是虚拟机栈是为Java方法服务的,而本地方法栈是为Native方法服务的.
4.方法区
方法区是用来存储类结构信息(包括常量池、静态变量、构造函数等)的地方,类型信息是由类加载器在类加载是从类文件中提取出来的
方法区存在着垃圾回收,因为用户通过自定义加载器加载的一些类同样会成为垃圾,JVM会回收一个未被引用类所占的空间,以使方法区的空间最小
方法区是线程共享的.
5.堆
堆是存储java实例或者对象的地方,是GC的主要区域,同样也是线程共享的内存区域.
下面举几个例子来说明:
上面的main方法中运行过程如下:
- 用户创建了一个Student对象,运行时JVM首先到方法区寻找该对象的类型信息,没有则使用类加载器将Student.class字节码文件加载至6内存中的方法区,并将Student类的类型信息存放至方法区
- 然后JVM在堆中为新的Student实例分配内存空间,这个实例持有着指向方法区的Student类型信息的引用(类型信息在方法区中的内存地址)
- 在运行的JVM进程中,会首先运行一个线程执行此用户程序,而创建线程的同时也创建了一个虚拟机栈,虚拟机栈用来跟踪线程运行中的一系列方法调用过程,每调用一个方法就会创建一个栈帧并压入栈中. 上面的stu是对Student的引用,存放在栈中,指向堆中Student实例的内存地址.
- JVM根据stu引用持有的堆中对象地址,定位到堆中的Student实例,由堆中实例指向的方法区Student类型信息引用,获得add()方法的字节码信息,然后就可以执行add()方法的指令了
总结
一、方法区、堆是线程共享的。 虚拟机栈、本地方法栈和程序计数器是线程私有的
二、栈中:
三、堆中:
- 存储的全是对象实例, 每个对象包含一个与之对象的class信息,存放在方法区
- JVM只有一个堆区,被所有线程共享,堆区中不存放基本类型和对象引用,只存放对象实例
四、方法区中:
- 存放线程执行的字节码指令
- 被所有线程共享,其中包含所有的class和static变量
- 常量池位于方法区中,例如String的常量池