JVM内存模型(线程栈)

java初级程序员和中级程序员的分界的知识点有:Linux、JVM、多线程、设计模式,所以最近开始啃JVM,以下文字留作笔记以供复习。

Java内存模型顾名思义,"模型"表明这是一个虚拟出来的东东,jvm规范虚拟出这个模型的目的主要是为了屏蔽计算机硬件层面的概念,毕竟JVM的目的就是干这个的。

在计算机硬件层面的内存的概念稍微说一点,之前学校里学的都忘得差不多了(其实当初估计也没认真听课)。而且另一方面如今计算机的硬件架构的更新也比较快。硬件系统的内存一般有:CPU寄存器、高速缓存、内存。一般高速缓存又有3级:L1,L2,L3,CPU又有多核,所以硬件本身就是存在内存读写不一致的问题,但是硬件之上的操作系统(操作系统也是一个软件)有自己的解决办法(总线加锁、intel提出的 MESI协议)去保证读写一致,所以操作系统在运行过程中能保证读写一致,保证操作系统运行结果符合预期。

Java的程序是运行在JVM中的,而JVM中的线程最终也会交给硬件CPU去执行。
具体的流程是:我们在使用Java线程,内部会调用操作系统(OS)的内核线程(Kernel-Level Thread),这种线程是操作系统内核(Kernel)直接支持的,内核通过调度器,对线程进行调度,并将线程交给各个CPU内核去处理。

因此Java开发的软件也需要解决因为硬件内存不一致这种问题。

所以JVM中多线程会存在读写不一致的问题,我们的解决办法有哪些呢?

以上说这么多就是为了引出Java内存模型的概念。这就是上面为什么说Java内存模型是一个虚拟出来的东东,目的是为了屏蔽计算机硬件的内存架构。或者说作为Java coder,你只需要了解Java内存模型,能在这个模型中保证读写一致,底层的硬件内存自然就读写一致了。以不变应万变,以不变的Java内存模型去屏蔽硬件内存架构的万变。

 

 

 

JVM(java virtual machine)是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现,并且遵循java 虚拟机规范。也就是说只要遵循java虚拟机规范开发出来的虚拟机实现都成为JVM。例如SUN的Hotspot,BEA的JRocket、IBM的JV9等。有关虚拟机的发展历史再这里就不细说了。

java虚拟机规范也会有改变,所以有些JVM的概念是适用于特定的版本的, 比如字符串常量的存放位置已经从jdk6中的方法区移到jdk7中的堆中。而且不同厂商生产的java虚拟机在遵守JVM规范的同时也有自己的个性实现。以下笔记主要基于现在主流的JVM Hotspot的实现来阐述的。

 

JVM的内存模型可以分为线程栈、方法区、堆。

栈是运行时单位,而堆是存储的单元。

 

一、线程栈

栈是运行时单位,栈是线程私有的,所以称为线程栈。栈(Stack)是由栈帧(Stack Frame)组成。每个方法的调用都对应一个栈帧。栈帧的存储空间分配在Java虚拟机栈之中,每一个栈帧都有:局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。

局部变量表包括各种基本数据类型:boolean、byte、char、short、int、float、long、double以及对象的引用。

线程栈由栈帧组成。栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。 栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

 

每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。在一条线程之中,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称为是当前栈帧(Current Frame),这个栈帧对应的方法就被称为是当前方法(Current Method),定义这个方法的类就称作当前类(Current Class)。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的对局部变量表和操作数栈进行的操作。如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。当一个新的方法被调用,一个新的栈帧也会随之而创建,并且随着程序控制权移交到新的方法而成为新的当前栈帧。当方法返回的之际,当前栈帧会传回此方法的执行结果给前一个栈帧,在方法返回之后,当前栈帧就随之被丢弃,前一个栈帧就重新成为当前栈帧了。

栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一条线程的栈帧。

 

 

(1)局部变量表 

 

每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表。栈帧中局部变量表的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用(这个概念暂时没搞懂)。

一个局部变量(Slot)可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零至小于局部变量表最大容量的所有整数。long和double类型的数据占用两个连续的局部变量,这两种类型的数据值采用两个局部变量之中较小的索引值来定位。

Java虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从0开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即Java语言中的“this”关键字)。后续的其他参数将会传递至从1开始的连续的局部变量表位置上。

 

(2)操作数栈

 

每一个栈帧内部都包含一个称为操作数栈(Operand Stack)的后进先出(Last-In-First-Out,LIFO)栈。栈帧中操作数栈的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。

在上下文明确,不会产生误解的前提下,我们经常把“当前栈帧的操作数栈”直接简称为“操作数栈”。操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。

 

举个例子,iadd字节码指令的作用是将两个int类型的数值相加,它要求在执行的之前操作数栈的栈顶已经存在两个由前面其他指令放入的int型数值。在iadd指令执行时,2个int值从操作栈中出栈,相加求和,然后将求和结果重新入栈。在操作数栈中,一项运算常由多个子运算(Subcomputations)嵌套进行,一个子运算过程的结果可以被其他外围运算所使用。

每一个操作数栈的成员(Entry)可以保存一个Java虚拟机中定义的任意数据类型的值,包括long和double类型。

 

在操作数栈中的数据必须被正确地操作,这里正确操作是指对操作数栈的操作必须与操作数栈栈顶的数据类型相匹配,例如不可以入栈两个int类型的数据,然后当作long类型去操作他们,或者入栈两个float类型的数据,然后使用iadd指令去对它们进行求和。有一小部分Java虚拟机指令(例如dup和swap指令)可以不关注操作数的具体数据类型,把所有在运行时数据区中的数据当作裸类型(Raw Type)数据来操作,这些指令不可以用来修改数据,也不可以拆散那些原本不可拆分的数据,这些操作的正确性将会通过Class文件的校验过程来强制保障。

 

在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度。

 

你可能感兴趣的:(JVM)