文章目录
1. Java内存的概念
内存是非常重要的系统资源,是硬件以及CPU的中间仓库以及桥梁,承载着操作系统以及应用程序的实时运行。JVM的内存布局规定了Java在运行过程中内存申请,分配,管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。其中最为典型的不同主要是针对于方法区;
2. JVM的经典内存布局
- Java虚拟机在执行Java程序的时候会把他所管理的内存区域划分为若干个不同的数据区域;这些区域各有各的用途,有的区域随着虚拟机进程的启动就会一直存在,但是有的区域则是依赖于用户线程的启动和结束而创建和销毁;下图根据《Java虚拟机规范》展示了Java虚拟机是如何对自己管理的内存进行划分的;以及每一个数据区域内部的划分细节;
程序计数器
-
JVM中的程序计数器(Program Counter Register;PC Register)的命名来源于CUP的寄存器,但是这里的程序计数器并不是广义上的物理寄存器,而仅仅是对物理PC寄存器的一种抽象模型;它是一块较小的内存空间,几乎可以忽略不计,也是运行速度最快的内存区域;它可以看作是当前线程的字节码行号指示器;在Java虚拟机的概念模型里,字节码解释器的工作就是通过改变程序计数器的值来选取下一条需要执行的字节码指令;他是程序控制流的指示器,程序中的各种分支,跳转,循环以及异常处理都需要依赖这个程序计数器来完成;由于Java虚拟机的多线程机制是通过线程轮流切换,分配处理器执行时间(时间片)来实现的,所以说在任何的一个时刻,一个处理器(多处理器对应为一个内核)都只会执行一条特定的线程中的指令,在这个线程的时间片用完之后或者说因为其他原因(比如说等待I/O)而放弃在处理器上运行的时候,就会大发生线程切换,为了线程切换之后能够恢复到正确的执行位置;每一条线程都需要有一个独立的程序计数器(所以说程序计数器的生命周期与线程的生命周期是保持是一致的),各个线程之间的程序计数器互不干扰,互不影响,独立存储;我们称这类内存区域为线程私有的内存区域;需要注意的是,如果当前线程执行的是一个Java方法的话,那么程序计数器中存储的值就是当前正在执行的虚拟机字节码指令的地址;如果当前线程正在执行的是一个本地方法的话,那么程序计数器中存储的值将会是一个未定义的值,也就是空(Undefined);由于该内存区域很小,仅仅是存储一个虚拟机字节码指令的地址,所以说该区域也是唯一一个一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的内存区域。
-
代码示例:在此我们利用javap -v xxx.class反编译字节码文件,查看指令等信息;
-
面试中常问的两个问题:
- 使用PC寄存器存储字节码指令有什么用呢;(为什么使用PC寄存器来记录当前线程的虚拟机字节码指令地址呢):还是上面所提到的,由于Java虚拟机对多线程机制是通过线程轮流切换,分配处理器执行时间的方式来实现的,当程序在运行的时候,总会发生线程切换,所以说为了确保当前线程被切换回来的时候线程能够确定接下来该从哪儿开始执行;我们需要在PC寄存器中存储当前线程执行到的虚拟机字节码的指令,这时线程便可以知道字节该从哪儿开始执行了;
- 程序计数器为什么会被设定为线程私有的:如果程序计数器不是线程私有的,那么在线程被切换后程序计数器的值就会被其他线程改变,那么在线程切换回来的时候,该线程就不能够正确地执行它余下的代码,同时所有线程的执行将会无法调节,所以程序计数器应该被设计为线程私有的,这样的话每一个线程的程序计数器便会互不影响,独立存储;
Java虚拟机栈
- 由于当年Java团队在设计Java的时候立的flag就是"一次编译,到处运行",因此Java需要被实现为跨平台的,为了实现这一功能,Java的指令都是根据栈来设计的,由于不同CPU的架构是不同的,因此不能把Java指令设计为基于寄存器的;Java虚拟机栈的设计的优点就是跨平台,指令集小,编译器容易实现;而Java虚拟机栈也不可避免的存在一些缺点:相比较于基于寄存器的指令实现性能会有所将下降,同时完成同一个功能需要更多的指令;同时,很多人会把Java的内存区域笼统地划分为堆和栈,这种划分方式直接继承自传统的C/C++程序的内存布局;但是这样的划分在Java语言里面就显得尤为粗糙了,实际的内存区域划分要比这复杂的多,更多的细节会在下文进行讲解。在这里我们需要了解Java中栈和堆的区别,需要明确的是,栈是运行时的单位,而堆则是存储时的单位;栈解决了程序执行的问题,如程序该如何执行,或者说程序应该如何对数据进行处理;而堆则解决了数据存储的问题,即数据应该怎么放,放在哪儿。
- 与程序计数器一样,Java虚拟机栈也是线程私有的,因此它的生命周期与线程的生命周期是一致的。虚拟机栈描述的是Java方法在执行过程中的线程内存模型:每当一个方法被执行的时候,Java虚拟机就会为该方法同步地创建一个栈帧用于存储该方法相应的局部变量表,操作数栈,动态链接以及方法返回地址等信息(即每一个栈帧对应着每一次的Java方法调用)。每一个Java方法被调用直至运行完毕的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程;Java虚拟机栈栈帧的大小主要取决于局部变量表的大小;
- Java虚拟机栈的存储结构以及运行原理:每一个线程都有自己的栈空间,栈中的数据都是以栈帧的形式存在的,在这个线程上正在执行的每一个方法都对应有一个栈帧;栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种信息,Java虚拟机对虚拟机栈的操作只有入栈和出栈两种;在一条线程执行的过程中,一个时间点上只会有一个活动的栈帧,即只有当前执行方法所对应的栈帧(栈顶栈帧)是有效的,这个栈帧称之为当前栈帧,这个方法被称之为当前方法,定义当前方法的类被称之为当前类;同时,执行引擎运行的所有字节码指令只针对当前栈帧进行操作,不同线程所包含的栈帧是不允许存在相互引用的,即不可能在一个栈桢之中引用另一个线程的栈帧;如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧;Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常。不管使用哪一种方式,都会导致栈帧被弹出;
- 局部变量表:局部变量表也被称作为局部变量数组或者是本地变量表,它被定义为一个数组,局部变量表中存放了编译期可知的各种Java基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型,他并不等同于Java对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或者其他于此对象相关的位置)和returnAddress类型(指向的是一条字节码的指令地址),它们可能是方法参数或者是定义在方法内部的局部变量;这些数据类型在局部变量表中的存储空间以局部变量槽来表示;其中64位程度的long和double数据类型会占用两个变量槽的大小,其余的数据类型只会占用一个变量槽。局部变量表所需要的内存空间在编译期间就已经完成分配了,并保存在方法的Code属性的maximum local variables数据项中,当进入一个方法是,这个方法需要在栈帧中分配多大的局部变量表空间是已经完全确定的,在方法执行的过程中不会改变局部变量表的大小,需要注意的是,这里提到的大小并不是空间大小,而是变量槽的数量,因为变量槽的空间大小完全决定于具体的虚拟机是如何实现的;局部变量表中的变量只在当前方法调用中有效,在方法执行时,虚拟机通过局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束之后,随着方法栈帧的销毁,局部变量表也会随之销毁。
- 使用IDEA查看局部变量表:可以利用javap命令对字节码文件进行解析查看main()方法对应栈帧的局部变量表;也可以使用jclasslib byte viewcoder插件查看方法内部的字节码信息剖析;
- 对变量槽slot的理解:参数值的存放总是在局部变量数组的index[0]开始,到index[length - 1]的索引节点结束,而局部变量表最基本的存储单元是slot(变量槽),当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上,JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引就可以成功访问到局部变量表中指定的局部变量值,当我们需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可,需要注意的是,如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列,但是this变量不存在于静态方法的局部变量表中,所以在我们使用Java语言编写程序的时候,是无法在静态方法中使用this的;在此有的同学可能会感到好奇,如果局部变量太多怎么办,会不会因为分配过多的sl