深入理解jvm之一【内存区域】

        文章开始之前,首先需要申明,本系列文章讨论的是HotSpot VM,文章中多数观点基于《深入理解Java虚拟机:JVM高级特性与最佳时间   周志明》,笔者如有理解错误,欢迎指正。


        在开始探索jvm虚拟机之前,不得不对jvm的内存区域进行讨论,依旧先附上图表:


    程序计数器

        程序计数器,也能叫做PC寄存器,从名字上来理解可能会把它想成一个计数的内存区域,但是,了解汇编的人会知道,程序技术器实际上是CPU上的一个寄存器,它保存当前指令执行的地址(也可以说下一条指令所在的存储单元)。当cpu执行该线程时,会从程序计数器中取出当前指令所在的地址,之后自动加1或者指向像一条指令。依次执行,直至全部执行完毕。

        在支持多线程的系统中,线程时通过轮换来获得cpu的执行时间的,一个cpu只能执行一个线程,所以在轮换线程的时候,要确保被轮换的线程能过回到之前的状态,而程序计数器就能够起到这个作用,当线程被轮换的时候,计数器记录了一下条指令的执行地址,所以,只需要获取该指令就能回到之前的状态。很明显,该变量属于线程所独有的。

        在jvm规范中规定,在调用java方法时,程序计数器会记录下一条虚拟机字节码指令的地址;如果执行的是Native方法,则该程序计数器的值为"Undefined"。并且此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

      Java虚拟机栈

        栈做为一个常用的数据结构,有先进后出的特点。在jvm中,Java虚拟机栈是Java方法执行的一个内存模型:每个Java方法的执行的同时创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直至执行完毕,都对应一个栈帧的进栈和出栈的操作,所以当前执行的方法总是在栈顶。说到这,想我写递归函数数经常会抛出:OverStackOverflowError,这是因为栈的深度已经超过了jvm规定Java虚拟机栈深度的最大值。

        在JVM规范中,对这片区域定义了两个类型的异常:

                1.因为栈太深,导致新的栈帧无法进栈,则抛出StackOverflowError。

                2.因为栈动态扩展的时候,没有多余的空闲内存用来分配给栈,则会抛出OutOfMemoryError。

        但是仔细想来,两者之间是存在交集的,例如:因为栈深,请求扩展,但是,空闲空间不够。这种情况两者都符合。查看相关博客知道:当是处于单线程的时候,无论是哪种原因导致进栈失败,都会抛出StackOverflowError。而在多线程的环境下,才可能会抛出OutOfMemoryError。

        下图是虚拟机栈和栈帧的概念展示:


        可以看到,一个栈帧中存在局部变量表,操作数栈,动态链接,方法出口和一些附加信息等,接下来,会一一对他们进行介绍:

        局部变量表

        顾名思义,存储了局部变量,例如方法形参,局部变量,returnAdress等,存储的类型可以是boolean,byte,char,short,int,float,reference,returnAdress和long,double等类型。局部变量表的基本单位叫做变量槽(Variable Slot),java虚拟机规范中并没有明确指出它应该多大,但是很有导向的指出了每一个都应该存储boolean,byte,char,short,int,float,reference,returnAdress类型,而long,double应该有两个Slot一块对它们的值进行存储。对于64位用两个Slot进行存储的数据,虚拟机采用高位对齐的方式为其分配两个Slot连续空间。

        虚拟机才用索引定位的方式访问局部变量表,它的索引值从0开始直至局部变量表最大的Slot数量。如果访问的是32位及以下大小的数据,则索引n就代表访问第n个Slot,假如是64位的数据类型,则会同时访问第n和第n+1的Slot。对于一个存放64位数据类型的两个连续的Slot,不允许值范文其中的一个,虚拟机规范中明确要求了,遇到这种操作的字节码序列,虚拟机应该在类加载阶段抛出异常。

        为了尽可能的节省栈帧空间,Slot是可以重复使用的,方法体中定义的变量,作用域不一定是覆盖整个方法体的,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量所占用的Slot可以分配给其他变量使用,当然,它还会伴随一些副作用。例如会影响到垃圾回收。

        操作数栈

        操作数栈,也叫做操作栈。操作数栈早方法执行的时候,栈是空的,当方法开始调用的时候,会有各种字节码指令会往操作数栈中写入和提取内容,这对应着栈的进栈和出栈操作。操作数栈与局部变量一样,会把boolean,byte,char,short.float,refenrece,returnAdress转化成int值进行存储,而64位的数据类型则占用两个存储空间。但是与局部操作数栈不同的是,它不是根据索引对存储的值进行操作而是根据栈的进出栈进行操作,由于java虚拟机的指令是从操作数。栈而不是寄存器中取值,所以它的运作方式是基于操作数栈而不是寄存器。

        动态链接

        每一个栈帧都包含一个指向运行时的常量池中该栈帧中所属的方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。在Class中存在大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用部分会在类加载或者第一次使用的时候转化为直接引用,这叫静态链接;当这些符号引用在运行时才转化成直接引用,这叫动态链接。

        方法出口(方法的出口地址)

        当方法调用后存在两种返回方式。第一种是运行到任意一个方法返回的字节码指令,这时可能会安装返回指令中的返回类型,将制定返回值返回给上一层,这种方式称为正常完成出口;第二种,当遇到抛出的异常,并且没有被及时捕获,导致方法退出,这种方式称为异常完成出口。这种方式的返回不会将任何值返回给上一层。

        附加信息

        虚拟机规范允许虚拟机的具体实现添加一些规范之外的信息到栈帧中。

   本地方法栈

        本地方法栈与java虚拟机栈功能类似,区别在于,虚拟机是为java方法服务的,而本地方法栈是为Native方法服务的,虚拟机规范对它的实现没要强制要求,所以,虚拟机可以自由的实现它。甚至有的虚拟机直接把它和虚拟机栈合二为一。它也会抛出StackOverflowError和OutOfMemeryError异常。

    Java堆

        这是虚拟机内存管理中最大的一块。具体什么用呢。首先它是所有线程锁共享的区域,在我们代码中创建的实例和数组基本全部在这个区域上分配,也就是说除了static修饰的new 空间,其余通过new 分配的区域基本都在这进行分配。

        这一块区域是垃圾收集器管理的主要区域,因此很多时候被叫做“GC堆”(Garbage Collection Heap)。从回收的角度来看,还会将它细分两个代:新生代和老年代。其中新生代还可以再细分为一个Eden和两个Survivor区域等。从内存分配的角度,Java堆还可以划分出对个线程私有的分配缓冲区。

        虚拟机规范规定,Java堆可以处在不连续的物理内存空间中,只需要逻辑上是连续的就ok。当然它的大小我们是可以动态调整的(通过-Xmx和-Xms)。当没有足够的内存可以用来完成实例的分配并且堆无法扩展时,将会抛出OutOfMemoryError异常。

    方法区

        方法区也是线程共享的区域。它会被用来存储虚拟机加载的类信息,常量,静态常量,即时编译后的代码等数据。虚拟机规范中,把它和java堆划分在一个区域,但是,它却有一个Non-heap(非堆)的名字。它也是一个可动态分配空间的(通过-XX:MaxPerSize和-XX:PerSize),它也可以像Java堆一样,被垃圾回收管理器进行管理,不过它却要复杂得多,而且苛刻得多,因为种种得限制,它的内存回收效率非常低。

    运行时常量池

        它是方法区的一部分,存储了编译期间的各种字面量和符号引用,这部分内容在类加载到方法区运行时常量池中存放。常量池中的内容可以在运行期间添加,如利用String类的intern()添加字符串,因此,它具有重要的特征:动态性。当它无法申请到内存时,会抛出:OutOfMemoryError.

    直接内存

        直接内存不是虚拟机运行时数据区域的一部分,也不是Java虚拟机规范中定义的内存区域。

        在jdk1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,能够在一些场景显著提高性能,因为避免了在Java堆和Native堆中来回复制。当动态扩展,内存不够分配时,会导致抛出OutOfMemoryError异常。(主要摘自《深入理解Java虚拟机第二版》)。

        这块内容是理解jvm最基本的内容,是理解jvm的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值