程序计数器(Program Counter Register),
也叫PC寄存器,是一块较小的内存空间,它可以看作是
当前线程所执行的字节码指令的行号指示器。字节码解释器的工作就是通过改变这个计数器的值来选取
下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完
成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定
的时刻,一个处理器(针对多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线
程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互
不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果一个线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是一个Native方法,这个计数器的值则为空。
此内存区域是唯一一个在Java的虚拟机规范中没有规定任何OutOfMemoryError异常情况的区域。
Java虚拟机栈
虚拟机栈也是线程私有,而且生命周期与线程相同,每个Java方法在执行的时候都会创建一个栈帧
(Stack Frame)。
1.栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变
量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一
个栈帧在虚拟机栈里从入栈到出栈的过程。
一个线程中方法的调用链可能会很长,很多方法都同时处于执行状态。对于JVM执行引擎来说,在在活
动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关连的方法
称为当前方法,定义这个方法的类叫做当前类。
执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。如果当前方法调用了其他方法,或者当前
方法执行结束,那这个方法的栈帧就不再是当前栈帧了。
调用新的方法时,新的栈帧也会随之创建。并且随着程序控制权转移到新方法,新的栈帧成为了当前栈
帧。方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧(返回给方法调用者),随后虚拟机将会
丢弃此栈帧。
2.局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变
量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占
用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和
returnAddress类型的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、
jsr_w和ret指令服务的,目前已经很少使用了。
虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是
32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储
3.操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数
栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。
操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2
个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表
或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表
或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈
的过程。
4.动态链接
在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直
接引用,而符号引用存在于方法区中的运行时常量池。
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的
目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解
析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
5.方法返回
当一个方法开始执行时,可能有两种方式退出该方法:
正常退出:是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过throw
语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返
回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根
据该方法返回的字节码指令确定。
异常退出:是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出
无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法
返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态
方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局
部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整PC计数器的值以指
向方法调用指令后的下一条指令.
一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法
异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息
6.附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范中没有描述的信息到栈帧之中,例如和调试相关的信
息,这部分信息完全取决于不同的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其
他附加信息一起归为一类,称为栈帧信息
7.栈异常
Java虚拟机规范中,对该区域规定了这两种异常情况:
1. 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;
2. 虚拟机栈可以动态拓展,当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
本地方法栈
本地方法栈和虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法栈
为虚拟机使用到的Native方法(比如C++方法)服务.
本地方法:该方法实现由非java实现。
为什么要使用本地方法:
1.某些层次任务用java实现不容易,或者有效率问题。
2.有时某些java应用需要和java外面的环境交互。
java 堆
Java堆被所有线程共享,在Java虚拟机启动时创建。是虚拟机管理最大的一块内存。
唯一目的是存放对象实例,Java虚拟机规范的描是:所有的对象实例以及数组都要在堆上分配
随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变
化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
Java堆是垃圾回收的主要区域,主要采用分代回收算法
堆分类
<1.8
新生代(Eden空间, From Survior空间, To Survior空间)
老年代
永久代
>=1.8
新生代
老年代
元空间
在jdk1.8中移除整个永久代(堆内存),取而代之是元空间(物理内存)
堆内存划分
堆大小 = 新时代+老年代。堆的大小可以通过-Xms -Xmx指定,其中新生代被细分为Eden和两个survior区
这两个区域分别以from 和to区分。默认: Eden:from:to=8:1:1 (或-XX:SurviorRatio指定)。jvm每次只使用其中
一块survior区域,执行gc是 某块对象复制到另一块。新生代实际可用空间90%。
对象创建一般过程:
对象的访问:
1.句柄:稳定,对象移动只需要修改句柄的地址。
2.直接指针:访问速度快,节省指针定位开销。
数组内存分析:
一维数组
二维数组