一、内存结构
1.1 程序计数器
定义:
- 程序计数器是一块较小的空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(可以理解为保存当前线程下一条jvm指令的地址)。
- 由于java虚拟机在多线程模式下是
通过线程抢占cpu时间片
的方式实现的,因此当某一线程执行完毕时,就要记住该线程的待执行指令
,等待cpu下次分配到时间片再从记录的指令处继续执行
。
特点:
- 线程私有
程序计数器是
每个线程私有的
,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该继续执行哪一条指令。
- 不存在内存溢出问题
程序计数器内存区域是虚拟机中唯一没有规定OutOfMemory情况的区域。
1.2 虚拟机栈
定义:
每个线程执行所需要的内存
,称为虚拟机栈;每个线程执行时,都会创建一个虚拟机栈。每个栈是由多个栈帧组成
,对应着线程中每个方法执行时所需要的内存。- 每个虚拟机栈
只能有一个活动栈帧
,对应着当前正在执行的那个方法。作用:主管Java程序的运行,保存方法的局部变量(基本数据类型或引用对象的地址)、部分结果,并参与方法的调用和返回。
特点:
- 栈是一种快速有效地存储方式,访问速度仅次于PC计数器
虚拟机栈是线程私有的
- JVM虚拟机直接对栈的操作只有两个:
- 方法执行时进栈
- 方式执行结束后出栈
- 虚拟机栈不存在垃圾回收问题,但存在栈内存溢出OOM
虚拟机栈容易出现的问题:
- JVM允许虚拟机栈的大小是动态或固定不变,不同情况下可能出现不同的问题:
StackOverFlowError:
采用固定大小的栈时,线程请求分配的栈容量超过虚拟机允许的栈容量大小OutOfMemeoryError:
当虚拟机栈的内存是动态拓展时,且在尝试拓展的时候无法申请到足够的内存或者没有足够的内存空间创建对应的虚拟机栈。- 通过
Xss选项
可以设置线程运行时的最大栈空间。
虚拟机栈运行原理:
一个活动线程中,一个时间点上,只会存在一个活动栈帧,即只有正在执行的方法应着的栈帧是有效的(
当前栈帧
)。
执行引擎运行的字节码执行只针对当前栈帧进行操作
。如果该方法调用了其他方法,对应的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧。当前方法执行完成之后,返回结果给前一个栈帧,接着虚拟机栈丢弃当前栈帧,前一个栈帧重新称为当前栈帧。不同虚拟机栈中的栈帧不允许存在相互应用
,即不可能在一个栈帧中引用另外一个线程的栈帧。- 方法的正常执行返回和抛出异常都会导致栈帧被弹出。
栈帧的内部结构
栈帧是内存区块
,是一个数据集,维系着方法执行过程中的各种数据信息:
- 局部变量表
- 操作数栈
- 动态链接(指向运行时常量池的方法引用)
- 方法返回地址
- 附加信息
- 1.局部变量表
- 是一个数字数组,主要用于
存储方法参数和定义在方法中的局部变量
- 不存在数据安全问题,因为它是线程私有的(
针对的是变量表,不是局部变量
)
局部变量没有逃离方法的作用访问,那么它是线程安全的
。- 局部变量如果引用了对象,并且逃离了方法的作用访问,需要考虑线程安全。
- 局部变量表所需的容量大小是
在编译期确定下来
的,并保存在字节码文件中的Code属性中。在方法运行期间是不会改变局部变量表的大小的。- 最基本的存储单元是Slot(变量槽):
- 32位之内的类型(包括引用类型)只占用一个Slot,64位的类型(long和double)占两个slot。值得注意的是byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
- JVM会为局部变量表中每个Slot分配一个访问索引,通过该索引可以访问局部变量值
- 访问64位的局部变量值是,只需使用两个索引中的一个即可
只要被局部变量表中直接或间接引用的对象都不会被垃圾回收
(其中的变量作为垃圾回收的根节点)
变量插槽Slot
局部变量表在可以使用Jclasslib插件进行查看,也可以使用
javap -v ****.class
查看字节码文件
再补充一下变量的分类:
成员变量:使用之前均有默认初始化值
- 类变量:链接中的准备阶段会给变量类变量赋值;在后续的初始化阶段根据代码块中进行赋值
- 实例变量: 随着类的创建会在堆中分配实例变量空间,并进行默认赋值
局部变量:使用前必须显式赋值,否则编译不通过
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
29 1 4 s4 Ljava/lang/String;
- 2.操作数栈
- 每个独立的栈帧中还存在一个后进先出的操作数栈,也可以称之为表达式栈
- 在执行方法过程中,操作数栈根据字节码指令,向栈中写入或提取数据,即入栈(
push
) / 出栈(pop
),操作数栈相当于栈帧操作数据的一个容器
- 操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中临时变量的存储空间。
一个方法中的操作数栈刚开始是空的
,栈中任何一个元素都可以是任意的Java类型。- 32位的类型占用一个栈单位深度,64位的类型占用两个栈单位深度
- 操作数栈并非采用访问索引的方式进行数据访问,而是
只能通过标准的入栈和出栈操作进行数据访问
。- 如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配
,这有编译器在编译期间进行验证,同时在类加载的过程中的类检验阶段的数据流分析阶段进行再次验证- Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
栈顶缓存技术:
- 由于操作数栈是存储在内存中的,因此频繁地执行内存读写操作必然会影响执行其速度。为了解决这个问题,提出了栈顶缓存(Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。
- 3. 动态链接
动态链接、方法返回地址、附加信息被称为帧数据区
- 每一个栈帧都包含一个指向运行时常量池中该栈所属方法的引用,包含这个引用的目的是为了支持当前方法的代码能够实现动态链接。
- 在Java源文件编译成字节码文件时,局