简介
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。每个区域有各自的用途,创建时间以及销毁时间。有的区域随着虚拟机的进程启动而存在,有的则是依赖用户线程的启动和结束而建立和销毁。JVM所管理的内存分为以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。
jvm运行时内存分为两部分:线程共享内存和线程私有内存。其中java堆、方法区属于共享内存,程序计数器、Java虚拟机栈、本地方法栈属于线程私有内存
程序计数器
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- - 摘自《深入理解Java虚拟机》
有什么特点?
-
线程私有
-
是jvm(java虚拟机)规范里面唯一一个没有OutOfMemoryError情况的区域
-
生命周期随着线程,线程启动而产生,线程结束而消亡
做什么用的?
java是支持多线程的,当CPU执行权从 A 线程,转移到 B 线程的时候,JVM就要暂时挂起线程 A ,去执行线程 B ;当线程 A 再次得到CPU执行权的时候,又会挂起B线程,继续执行 A 线程 ;CPU根本就不会记住之前执行到哪里了,它只是埋头苦干;那是什么保证了切换线程的程序可以正常执行的?正是程序计数器,程序计数器里面保存的是当前线程执行的字节码的行号。
有代码如下
package com.xnccs.cn.share;
public class ShareCal {
public int calc(){
int a = 100;
int b = 200;
int c = 300;
return ( a + b ) * c;
}
}
这是一段非常简单的计算代码,我们先编译成Class 文件再使用 javap 反汇编工具看下class 文件中数据格式,如下图
图中使用红框框起来的就是字节码指令的偏移地址(为了理解起来方便,我们也称它为行号),偏移地址对应的bipush 等等是jvm 中的操作指令,这是入栈指令。 当执行到方法calc()时在当前的线程中会创建相应的程序计数器,在计数器中为存放执行地址 (红框中的)0 2 3…等等。
那么,我们需要几个程序计数器呢?如果,我们只有一个的话,切换B线程以后,程序计数器里面保存的就是B线程所执行的字节码的行号了,再切换回A线程,就蒙圈了,不知道执行到哪里了,因为,程序计数器里面保存的是B线程当前执行的字节码地址 ;因此,我们可以想象出,要为每个线程都分配一个程序计数器, 因此,程序计数器的内存空间是线程私有的 ;这样即使线程 A 被挂起,但是线程 A 里面的程序计数器,记住了A线程当前执行到的字节码的指令地址了 ,等再次切回到A线程的时候,看一下程序计数器,就知道之前执行到哪里了!
何时被创建及销毁?
那么程序计数器,什么时候分配内存呢?我们试想下,一个线程在执行的任何期间,都会失去CPU执行权,因此,我们要从一个线程被创建开始执行,就要无时无刻的记录着该线程当前执行到哪里了! 因此,线程计数器,必须是线程被创建开始执行的时候,就要一同被创建;
程序计数器,保存的是当前执行的字节码的偏移地址(也就是之前说的行号,其实那不是行号,是指令的偏移地址,只是为了好理解,才说是行号的,),当执行到下一条指令的时候,改变的只是程序计数器中保存的地址,并不需要申请新的内存来保存新的指令地址;因此,永远都不可能内存溢出的; 因此,jvm虚拟机规范,也就没有规定,也是唯一一个没有规定 OutOfMemoryError 异常 的区域;
当线程执行的是本地方法的时候,程序计数器中保存的值是空(undefined);原因很简单:本地方法是C++/C 写的,由系统调用,根本不会产生字节码文件,因此,程序计数器也就不会做任何记录 ;
Java虚拟机栈
局部变量表
存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)。
操作数栈
动态链接
方法返回地址
附加信息
本地方法栈
java堆
方法区
参考文章:
1.探究Java虚拟机栈