对于从事C、C++开发的程序员来讲,在内存管理领域,他们既是拥有最高权力的“皇帝”,又是从事最基础工做的劳动人民——既拥有每一个对象的“全部权”,html
又担负着每个对象从开始到终结的维护职责。java
对于java程序员来讲,在虚拟机自动内存管理机制的帮助下,再也不须要为没一个new操做去配对的free/delete(C、C++语言对对象的删除和内存释放操做),linux
不容易出现内存泄漏和内存溢出问题,看起来由虚拟机管理内存一切看起来很美好。不过,也正是java把控制内存的权力交给了java虚拟机,一旦出现内存泄漏程序员
和内存溢出方面的问题,若是不了解虚拟机是怎么使用内存的,那排查错误、修正问题将会成为一项异常艰难的工做。windows
java虚拟机在执行java程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。这些区域有各自的用途,以及建立和销毁的时间,有些区域会随着缓存
虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而创建和销毁。以下图所示:多线程
咱们知道JVM也属于一种特殊的操做系统,那这些数据区域跟咱们最经常使用的windows哪些部分相对应呢。咱们能够吧windows的CPU+缓存+主内存和JVM的执行引擎+oracle
操做数栈+(栈、堆)对应起来,这样更加利于咱们去理解JVM。jvm
从上图可见,java虚拟机栈是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是java方法执行的线程内存模型:每一个方法被执行的时候,java虚拟机都会spa
同步建立一个栈帧用于存储局部变量表、操做数栈、动态链接、返回地址等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到
出栈的过程。咱们来经过一段很是简短的代码来演示虚拟机栈的做用:
/** * @ClassName StackTest * @description: * @author:liuyi * @Date:2020/11/23 23:45 */ public class StackTest { public static void main(String[] args) { A(); } static void A(){ B(); } static void B(){ C(); } static void C(){ } }
当咱们运行main方法,虚拟机会开启一个线程,同时为当前线程划分一块内存区域做为当前线程的虚拟机栈。同时在执行每一个方法的时候都会打包成一个栈帧。
好比 main 开始运行,打包一个栈帧送入到虚拟机栈。C 方法运行完了,C 方法出栈,接着 B 方法运行完了,B 方法出栈、接着 A 方法运行完了,A 方法出栈,
最后 main 方法运行完了,main 方法这个栈帧就出栈了。这个就是 Java 方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的。而
每个方法对应一个栈帧。入栈过程以下图所示:
上图描述了整个main方法调用的入栈和出栈的过程,须要注意的是栈帧出栈以后就没了,栈帧没得GC的说法。
栈帧大致都包含四个区域:(局部变量表、操做数栈、动态链接、返回地址)
咱们来经过分析一个简单的方法来理解栈帧中各个区域是如何运做的,代码以下:
/**
* @ClassName User
* @description:
* @author:liuyi
* @Date:2020/11/25 20:51
*/
public class User {
public static int work(){
int a = 2;
int b = 3;
int c = a*b;
return c;
}
public static void main(String[] args) {
System.out.println(work());
}
}
当该程序运行的时候,JVM会为其分配虚拟机栈,并生成对应的栈帧,以下图所示:
咱们经过反汇编命令查看work方法的字节码以下:
咱们看到work方法一共由10条字节码组成,咱们来逐步分析。
打开 https://cloud.tencent.com/developer/article/1333540查看字节码指令
现来看iconst_2对应的含义,如图
因此第1个字节码是将一个值为2的数字加载到操做数栈。再来看 istore_0的含义,如图
因此第2个字节码的含义就是将第一步中放入到操做数栈的数字放到局部变量表中,位置为0。因此前面两个字节码对应的java代码就是int a = 2;那么显而易见3和4两个字节码对应的
就是int b = 3;到这里,你们内心确定会有疑问,为何不直接将值放到局部变量表呢?咱们接着分析,你就明白了。
继续来看第5和第6两个字节码:iload_0和iload_1,它们的含义是将局部变量表中位置0和1的两个数加载到操做数栈中,接着咱们来看关键的第7个字节码:imul,它表明的意思
是相乘,就是将操做数栈中的数字进行乘法运算,咱们知道相乘是须要运算的,因此此时要交给执行引擎运算,运算完成以后再将运算的结果返回到操做数栈。因此操做数栈的做用
就是为jvm高速的计算提供缓冲区。
接着来看第8个字节码:istore_2,它的含义就是将计算的结果放入局部变量表,到这里int c = a*b;就执行完了。而后再来看第9和第10个字节码,它们的含义是将局部变量表的值再
压入操做数栈,最后返回。至此,整个方法执行结束,以上就是栈帧中各个区域在方法执行中的运做流程。
虚拟机栈的大小缺省为 1M,可用参数 –Xss 调整大小,例如-Xss256k。
咱们能够看到linux的建议配置为1M,至于windows为啥没有,博主大胆猜测可能跟微软和Oracel两家公司竞争有关吧,毕竟微软开发.net就是和java竞争的。
StackOverflowError异常:若是线程请求的栈深刻大于虚拟机所容许的深度,将抛出StackOverflowError异常,一般是由无线递归致使的,以下面的代码
与虚拟机栈同样,程序计数器也是线程私有的。程序计数器是一块很小的内存空间,它能够看做是当前线程所执行的字节码的行号指示器,就如上面反汇编User.class看到的同样。每个字节码都有本身的序号:
如上图所示,虽然这些序号是由顺序的,可是并不必定是依次递增,若是某给字节码占用的空间很大,那么它的序号相较于前一个序号就差距更大。
在java虚拟机的概念模型里,字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,它是程序控制流的执行器,分支、
循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器完成。
它还有另一个做用,咱们知道在java中能够开启成百上千个线程,可是咱们通常的电脑CPU也就8个左右。java虚拟机的多线程是经过线程轮流切换、
分配处理器执行时间方式来实现的,那么切换后虚拟机是怎么知道之前运行的位置,继续运行的呢?这个时候,程序计数器就起到了决定性的做用,由于
程序计数器是线程独有的,因此不会相互影响,当切回到当前线程,根据程序计数器记录的序号,继续执行对应的字节码便可。
在JVM中,只有执行java方法的时候,程序计数器才会记录正在执行的虚拟机字节码指令的地址,若是正在执行的是本地(Native)方法,这个计数器
则应为空(Undefined)。可是这里会产生一个疑问,若是恰好在执行Native方法的时候线程切换了,那切回来以后该怎么找到对应的位置呢?这里,我猜想
JVM可能规定了 在执行Native本地方法的时候,禁止切换当前线程(如不正确,请指正)。xianc
本地方法栈与虚拟机栈的做用很是类似,其区别只是虚拟机栈为java方法服务,而本地方法栈专门为Native本地方法服务。须要注意的是,HotSpot直接把
本地方法栈和虚拟机栈合并了。
本篇文章介绍了JVM的内存区域之线程私有区域,主要介绍了虚拟机栈的各个组成部分以及java方法是怎么经过虚拟机栈来实现执行的,接着介绍了程序计数器的做用
最后简述了本地方法栈。下一章,咱们将要分析JVM内存区域的线程共享数据区,主要包括堆、方法区、运行时常量池以及直接内存等内容。