Java内存模型-堆和栈

Java内存模型-堆和栈

BangQ IT哈哈 java

  i.Java内存管理简介:

  内存管理在Java语言中是JVM自动操做的,当JVM发现某些对象再也不须要的时候,就会对该对象占用的内存进行重分配(释放)操做,并且使得分配出来的内存可以提供给所须要的对象。在一些编程语言里面,内存管理是一个程序的职责,可是书写过C++的程序员很清楚,若是该程序须要本身来书写颇有可能引发很严重的错误或者说不可预料的程序行为,最终大部分开发时间都花在了调试这种程序以及修复相关错误上。通常状况下在Java程序开发过程把手动内存管理称为显示内存管理,而显示内存管理常常发生的一个状况就是引用悬挂——也就是说有可能在从新分配过程释放掉了一个被某个对象引用正在使用的内存空间,释放掉该空间事后,该引用就处于悬挂状态。若是这个被悬挂引用指向的对象试图进行原来对象(由于这个时候该对象有可能已经不存在了)进行操做的时候,因为该对象自己的内存空间已经被手动释放掉了,这个结果是不可预知的。显示内存管理另一个常见的状况是内存泄漏,当某些引用再也不引用该内存对象的时候,而该对象本来占用的内存并无被释放,这种状况简言为内存泄漏。好比,若是针对某个链表进行了内存分配,而由于手动分配不当,仅仅让引用指向了某个元素所处的内存空间,这样就使得其余链表中的元素不能再被引用并且使得这些元素所处的内存让应用程序处于不可达状态并且这些对象所占有的内存也不可以被再使用,这个时候就发生了内存泄漏。而这种状况一旦在程序中发生,就会一直消耗系统的可用内存直到可用内存耗尽,而针对计算机而言内存泄漏的严重程度大了会使得原本正常运行的程序直接由于内存不足而中断,并非Java程序里面出现Exception那么轻量级。
  在之前的编程过程当中,手动内存管理带了计算机程序不可避免的错误,并且这种错误对计算机程序是毁灭性的,因此内存管理就成为了一个很重要的话题,可是针对大多数纯面向对象语言而言,好比Java,提供了语言自己具备的内存特性:自动化内存管理,这种语言提供了一个程序垃圾回收器(Garbage Collector[GC]),自动内存管理提供了一个抽象的接口以及更加可靠的代码使得内存可以在程序里面进行合理的分配。最多见的状况就是垃圾回收器避免了悬挂引用的问题,由于一旦这些对象没有被任何引用“可达”的时候,也就是这些对象在JVM的内存池里面成为了避免可引用对象,该垃圾回收器会直接回收掉这些对象占用的内存,固然这些对象必须知足垃圾回收器回收的某些对象规则,而垃圾回收器在回收的时候会自动释放掉这些内存。不只仅如此,垃圾回收器一样会解决内存泄漏问题。c++

  ii.详解堆和栈[图片以及部份内容来自《Inside JVM》]:

  1)通用简介

  [编译原理]学过编译原理的人都明白,程序运行时有三种内存分配策略:静态的、栈式的、堆式的
  静态存储——是指在编译时就可以肯定每一个数据目标在运行时的存储空间需求,于是在编译时就能够给它们分配固定的内存空间。这种分配策略要求程序代码中不容许有可变数据结构的存在,也不容许有嵌套或者递归的结构出现,由于它们都会致使编译程序没法计算准确的存储空间。
  栈式存储——该分配可成为动态存储分配,是由一个相似于堆栈的运行栈来实现的,和静态存储的分配方式相反,在栈式存储方案中,程序对数据区的需求在编译时是彻底未知的,只有到了运行的时候才能知道,可是规定在运行中进入一个程序模块的时候,必须知道该程序模块所须要的数据区的大小才能分配其内存。和咱们在数据结构中所熟知的栈同样,栈式存储分配按照先进后出的原则进行分配。
  堆式存储——堆式存储分配则专门负责在编译时或运行时模块入口处都没法肯定存储要求的数据结构的内存分配,好比可变长度串和对象实例,堆由大片的可利用块或空闲块组成,堆中的内存能够按照任意顺序分配和释放。
  [C++语言]对比C++语言里面,程序占用的内存分为下边几个部分:
  [1]栈区(Stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操做方式相似于数据结构中的栈。咱们在程序中定义的局部变量就是存放在栈里,当局部变量的生命周期结束的时候,它所占的内存会被自动释放。
  [2]堆区(Heap):通常由程序员分配和释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式却是相似于链表。咱们在程序中使用c++中new或者c中的malloc申请的一块内存,就是在heap上申请的,在使用完毕后,是须要咱们本身动手释放的,不然就会产生“内存泄露”的问题。
  [3]全局区(静态区)(Static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另外一块区域。程序结束后由系统释放。
  [4]文字常量区:常量字符串就是放在这里的,程序结束后由系统释放。在Java中对应有一个字符串常量池。
  [5]程序代码区:存放函数体的二进制代码git

  2)JVM结构【堆、栈解析】:

  在Java虚拟机规范中,一个虚拟机实例的行为主要描述为:子系统、内存区域、数据类型和指令,这些组件在描述了抽象的JVM内部的一个抽象结构。与其说这些组成部分的目的是进行JVM内部结构的一种支配,更多的是提供一种严格定义实现的外部行为,该规范定义了这些抽象组成部分以及相互做用的任何Java虚拟机执行所须要的行为。下图描述了JVM内部的一个结构,其中主要包括主要的子系统、内存区域,如同之前在《Java基础知识》中描述的:Java虚拟机有一个类加载器做为JVM的子系统,类加载器针对Class进行检测以鉴定彻底合格的类接口,而JVM内部也有一个执行引擎:
Java内存模型-堆和栈
  当JVM运行一个程序的时候,它的内存须要用来存储不少内容,包括字节码、以及从类文件中提取出来的一些附加信息、以及程序中实例化的对象、方法参数、返回值、局部变量以及计算的中间结果。JVM的内存组织须要在不一样的运行时数据区进行以上的几个操做,下边针对上图里面出现的几个运行时数据区进行详细解析:一些运行时数据区共享了全部应用程序线程和其余特有的单个线程,每一个JVM实例有一个方法区和一个内存堆,这些是共同在虚拟机内运行的线程。在Java程序里面,每一个新的线程启动事后,它就会被JVM在内部分配本身的PC寄存器[PC registers](程序计数器器)和Java堆栈(Java stacks)。若该线程正在执行一个非本地Java方法,在PC寄存器的值指示下一条指令执行,该线程在Java内存栈中保存了非本地Java方法调用状态,其状态包括局部变量、被调用的参数、它的返回值、以及中间计算结果。而本地方法调用的状态则是存储在独立的本地方法内存栈里面(native method stacks),这种状况下使得这些本地方法和其余内存运行时数据区的内容尽量保证和其余内存运行时数据区独立,并且该方法的调用更靠近操做系统,这些方法执行的字节码有可能根据操做系统环境的不一样使得其编译出来的本地字节码的结构也有必定的差别。JVM中的内存栈是一个栈帧的组合,一个栈帧包含了某个Java方法调用的状态,当某个线程调用方法的时候,JVM就会将一个新的帧压入到Java内存栈,当方法调用完成事后,JVM将会从内存栈中移除该栈帧。JVM里面不存在一个能够存放中间计算数据结果值的寄存器,其内部指令集使用Java栈空间来存储中间计算的数据结果值,这种作法的设计是为了保持Java虚拟机的指令集紧凑,使得与寄存器原理可以紧密结合而且进行操做。
Java内存模型-堆和栈程序员

  1)方法区(Method Area)

  在JVM实例中,对装载的类型信息是存储在一个逻辑方法内存区中,当Java虚拟机加载了一个类型的时候,它会跟着这个Class的类型去路径里面查找对应的Class文件,类加载器读取类文件(线性二进制数据),而后将该文件传递给Java虚拟机,JVM从二进制数据中提取信息而且将这些信息存储在方法区,而类中声明(静态)变量就是来自于方法区中存储的信息。在JVM里面用什么样的方式存储该信息是由JVM设计的时候决定的,例如:当数据进入方法的时候,多类文件字节的存储量以Big-Endian(第一次最重要的字节)的顺序存储,尽管如此,一个虚拟机能够用任何方式针对这些数据进行存储操做,若它存储在一个Little-Endian处理器上,设计的时候就有可能将多文件字节的值按照Little-Endian顺寻存储。算法

  ——【$Big-Endian和Little-Endian】——

  程序存储数据过程当中,若是数据是跨越多个字节对象就必须有一种约定:编程

  • 它的地址是多少:对于跨越多个字节的对象,通常它所占的字节都是连续的,它的地址等于它所占字节最低地址,这种状况链表可能存储的仅仅是表头
  • 它的字节在内存中是如何组织的
      好比:int x,它的地址为0x100,那么它占据了内存中的0x100、0x10一、0x10二、0x103四个字节,因此通常状况咱们以为int是4个字节。上边只是内存组织的一种状况,多字节对象在内存中的组织有两种约定,还有一种状况:若一个整数为W位,它的表示以下:
      每一位表示为:[Xw-1,Xw-2,...,X1,X0]
      它的最高有效字节MSB(Most Significant Byte)为:[Xw-1,Xw-2,...,Xw-8]
      最低有效字节LSB(Least Significant Byte)为:[X7,X6,...,X0]
      其他字节则位于LSB和MSB之间
      LSB和MSB谁位于内存的最低地址,即表明了该对象的地址,这样就引出了Big-Endian和Little-Endian的问题,若是LSB在MSB前,LSB是最低地址,则该机器是小端,反之则是大端。DES(Digital Equipment Corporation,如今是Compaq公司的一部分)和Intel机器(x86平台)通常采用小端,IBM、Motorola(Power PC)、Sun的机器通常采用大端。固然这种不能表明全部状况,有的CPU既能工做于小端、又能够工做于大端,好比ARM、Alpha、摩托罗拉的PowerPC,这些状况根据具体的处理器型号有所不一样。可是大部分操做系统(Windows、FreeBSD、Linux)通常都是Little Endian的,少部分系统(Mac OS)是Big Endian的,因此用什么方式存储还得依赖宿主操做系统环境。
    Java内存模型-堆和栈
      由上图能够看到,映射访问(“写32位地址的0”)主要是由寄存器到内存、由内存到寄存器的一种数据映射方式,Big-Endian在上图能够看出的原子内存单位(Atomic Unit)在系统内存中的增加方向为从左到右,而Little-Endian的地址增加方向为从右到左。举个例子:
      若要存储数据0x0A0B0C0D:

      Big-Endian:

      以8位为一个存储单位,其存储的地址增加为:
    Java内存模型-堆和栈
      上图中能够看出MSB的值存储了0x0A,这种状况下数据的高位是从内存的低地址开始存储的,而后从左到右开始增加,第二位0x0B就是存储在第二位的,若是是按照16位为一个存储单位,其存储方式又为:
    Java内存模型-堆和栈
      则能够看到Big-Endian的映射地址方式为:
    Java内存模型-堆和栈数组

  MSB:在计算机中,最高有效位(MSB)是指位值的存储位置为转换为二进制数据后的最大值,MSB有时候在Big-Endian的架构中称为最左最大数据位,这种状况下再往左边的内存位则不是数据位了,而是有效位数位置的最高符号位,不只仅如此,MSB也能够对应一个二进制符号位的符号位补码标记:“1”的含义为负,“0”的含义为正。最高位表明了“最重要字节”,也就是说当某些多字节数据拥有了最大值的时候它就是存储的时候最高位数据的字节对应的内存位置:
Java内存模型-堆和栈
  Little-Endian:
  与Big-Endian相对的就是Little-Endian的存储方式,一样按照8位为一个存储单位上边的数据0x0A0B0C0D存储格式为:
Java内存模型-堆和栈
  能够看到LSB的值存储的0x0D,也就是数据的最低位是从内存的低地址开始存储的,它的高位是从右到左的顺序逐渐增长内存分配空间进行存储的,若是按照十六位为存储单位存储格式为:
Java内存模型-堆和栈
  从上图能够看到最低的16位的存储单位里面存储的值为0x0C0D,接着才是0x0A0B,这样就能够看到按照数据从高位到低位在内存中存储的时候是从右到左进行递增存储的,实际上能够从写内存的顺序来理解,实际上数据存储在内存中无非在使用的时候是写内存和读内存,针对LSB的方式最好的书面解释就是向左增长来看待,若是真正在进行内存读写的时候使用这样的顺序,其意义就体现出来了:
Java内存模型-堆和栈
  按照这种读写格式,0x0D存储在最低内存地址,而从右往左的增加就能够看到LSB存储的数据为0x0D,和初衷吻合,则十六位的存储就能够按照下边的格式来解释:
Java内存模型-堆和栈
  实际上从上边的存储还会考虑到另一个问题,若是按照这种方式从右往左的方式进行存储,若是是遇到Unicode文字就和从左到右的语言显示方式相反。好比一个单词“XRAY”,使用Little-Endian的方式存储格式为:
Java内存模型-堆和栈
  使用这种方式进行内存读写的时候就会发现计算机语言和语言自己的顺序会有冲突,这种冲突主要是以使用语言的人的习惯有关,而书面化的语言从左到右就能够知道其冲突是不可避免的。咱们通常使用语言的阅读方式都是从左到右,而低端存储(Little-Endian)的这种内存读写的方式使得咱们最终从计算机里面读取字符须要进行倒序,并且考虑另一个问题,若是是针对中文而言,一个字符是两个字节,就会出现总体顺序和每个位的顺序会进行两次倒序操做,这种方式真正在制做处理器的时候也存在一种计算上的冲突,而针对使用文字从左到右进行阅读的国家而言,从右到左的方式(Big-Endian)则会有这样的文字冲突,另一方面,尽管有不少国家使用语言是从右到左,可是仅仅和Big-Endian的方式存在冲突,这些国家毕竟占少数,因此能够理解的是,为何主流的系统都是使用的Little-Endian的方式
  *:这里不解释Middle-Endian的方式以及Mixed-Endian的方式】**
  LSB:在计算机中,最低有效位是一个二进制给予单位的整数,位的位置肯定了该数据是一个偶数仍是奇数,LSB有时被称为最右位。在使用具体位二进制数以内,常见的存储方式就是每一位存储1或者0的方式,从0向上到1每一比特逢二进一的存储方式。LSB的这种特性用来指定单位位,而不是位的数字,而这种方式也有可能产生必定的混乱。
Java内存模型-堆和栈安全

  ——以上是关于Big-Endian和Little-Endian的简单讲解——

  JVM虚拟机将搜索和使用类型的一些信息也存储在方法区中以方便应用程序加载读取该数据。设计者在设计过程也考虑到要方便JVM进行Java应用程序的快速执行,而这种取舍主要是为了程序在运行过程当中内存不足的状况可以经过必定的取舍去弥补内存不足的状况。在JVM内部,全部的线程共享相同的方法区,所以,访问方法区的数据结构必须是线程安全的,若是两个线程都试图去调用去找一个名为Lava的类,好比Lava尚未被加载,只有一个线程能够加载该类而另外的线程只可以等待。方法区的大小在分配过程当中是不固定的,随着Java应用程序的运行,JVM能够调整其大小,须要注意一点,方法区的内存不须要是连续的,由于方法区内存能够分配在内存堆中,即便是虚拟机JVM实例对象本身所在的内存堆也是可行的,而在实现过程是容许程序员自身来指定方法区的初始化大小的。
  一样的,由于Java自己的自动内存管理,方法区也会被垃圾回收的,Java程序能够经过类扩展动态加载器对象,类能够成为“未引用”向垃圾回收器进行申请,若是一个类是“未引用”的,则该类就可能被卸载,
  而方法区针对具体的语言特性有几种信息是存储在方法区内的:数据结构

  【类型信息】:

  • 类型的彻底限定名(java.lang.String格式)
  • 类型的彻底限定名的直接父类的彻底限定名(除非这个父类的类型是一个接口或者java.lang.Object)
  • 不论类型是一个类或者接口
  • 类型的修饰符(例如public、abstract、final)
  • 任何一个直接超类接口的彻底限定名的列表
      在JVM和类文件名的内部,类型名通常都是彻底限定名(java.lang.String)格式,在Java源文件里面,彻底限定名必须加入包前缀,而不是咱们在开发过程写的简单类名,而在方法上,只要是符合Java语言规范的类的彻底限定名均可以,而JVM可能直接进行解析,好比:(java.lang.String)在JVM内部名称为java/lang/String,这就是咱们在异常捕捉的时候常常看到的ClassNotFoundException的异常里面类信息的名称格式。
      除此以外,还必须为每一种加载过的类型在JVM内进行存储,下边的信息不存储在方法区内,下边的章节会一一说明
  • 类型常量池
  • 字段信息
  • 方法信息
  • 全部定义在Class内部的(静态)变量信息,除开常量
  • 一个ClassLoader的引用
  • Class的引用

      【常量池】

      针对类型加载的类型信息,JVM将这些存储在常量池里,常量池是一个根据类型定义的常量的有序常量集,包括字面量(String、Integer、Float常量)以及符号引用(类型、字段、方法),整个长量池会被JVM的一个索引引用,如同数组里面的元素集合按照索引访问同样,JVM针对这些常量池里面存储的信息也是按照索引方式进行。实际上长量池在Java程序的动态连接过程起到了一个相当重要的做用。多线程

      【字段信息】

      针对字段的类型信息,下边的信息是存储在方法区里面的:

  • 字段名
  • 字段类型
  • 字段修饰符(public,private,protected,static,final,volatile,transient)

      【方法信息】

      针对方法信息,下边信息存储在方法区上:

  • 方法名
  • 方法的返回类型(包括void)
  • 方法参数的类型、数目以及顺序
  • 方法修饰符(public,private,protected,static,final,synchronized,native,abstract)
     针对非本地方法,还有些附加方法信息须要存储在方法区内:
  • 方法字节码
  • 方法中局部变量区的大小、方法栈帧
  • 异常表

      【类变量】

      类变量在一个类的多个实例之间共享,这些变量直接和类相关,而不是和类的实例相关,(定义过程简单理解为类里面定义的static类型的变量),针对类变量,其逻辑部分就是存储在方法区内的。在JVM使用这些类以前,JVM先要在方法区里面为定义的non-final变量分配内存空间;常量(定义为final)则在JVM内部则不是以一样的方式来进行存储的,尽管针对常量而言,一个final的类变量是拥有它本身的常量池,做为常量池里面的存储某部分,类常量是存储在方法区内的,而其逻辑部分则不是按照上边的类变量的方式来进行内存分配的。虽然non-final类变量是做为这些类型声明中存储数据的某一部分,final变量存储为任何使用它类型的一部分的数据格式进行简单存储。

      【ClassLoader引用】

      对于每种类型的加载,JVM必须检测其类型是否符合了JVM的语言规范,对于经过类加载器加载的对象类型,JVM必须存储对类的引用,而这些针对类加载器的引用是做为了方法区里面的类型数据部分进行存储的。

      【类Class的引用】

      JVM在加载了任何一个类型事后会建立一个java.lang.Class的实例,虚拟机必须经过必定的途径来引用该类型对应的一个Class的实例,而且将其存储在方法区内

      【方法表】

      为了提升访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还添加一些其余的数据结构,如方法表【下边会说明】。

      2)内存栈(Stack):

      当一个新线程启动的时候,JVM会为Java线程建立每一个线程的独立内存栈,如前所言Java的内存栈是由栈帧构成,栈帧自己处于游离状态,在JVM里面,栈帧的操做只有两种:出栈和入栈。正在被线程执行的方法通常称为当前线程方法,而该方法的栈帧就称为当前帧,而在该方法内定义的类称为当前类,常量池也称为当前常量池。当执行一个方法如此的时候,JVM保留当前类和当前常量池的跟踪,当虚拟机遇到了存储在栈帧中的数据上的操做指令的时候,它就执行当前帧的操做。当一个线程调用某个Java方法时,虚拟机建立而且将一个新帧压入到内存堆栈中,而这个压入到内存栈中的帧成为当前栈帧,当该方法执行的时候,JVM使用内存栈来存储参数、局部变量、中间计算结果以及其余相关数据。方法在执行过程有可能由于两种方式而结束:若是一个方法返回完成就属于方法执行的正常结束,若是在这个过程抛出异常而结束,能够称为非正常结束,不管是正常结束仍是异常结束,JVM都会弹出或者丢弃该栈帧,则上一帧的方法就成为了当前帧。
      在JVM中,Java线程的栈数据是属于某个线程独有的,其余的线程不可以修改或者经过其余方式来访问该线程的栈帧,正由于如此这种状况不用担忧多线程同步访问Java的局部变量,当一个线程调用某个方法的时候,方法的局部变量是在方法内部进行的Java栈帧的存储,只有当前线程能够访问该局部变量,而其余线程不能随便访问该内存栈里面存储的数据。内存栈内的栈帧数据和方法区以及内存堆同样,Java栈的栈帧不须要分配在连续的堆栈内,或者说它们多是在堆,或者二者组合分配,实际数据用于表示Java堆栈和栈帧结构是JVM自己的设计结构决定的,并且在编程过程能够容许程序员指定一个用于Java堆栈的初始大小以及最大、最小尺寸。

      【概念区分】

  • 内存栈:这里的内存栈和物理结构内存堆栈有点点区别,是内存里面数据存储的一种抽象数据结构。从操做系统上讲,在程序执行过程对内存的使用自己经常使用的数据结构就是内存堆栈,而这里的内存堆栈指代的就是JVM在使用内存过程整个内存的存储结构,多指内存的物理结构,而Java内存栈不是指代的一个物理结构,更多的时候指代的是一个抽象结构,就是符合JVM语言规范的内存栈的一个抽象结构。由于物理内存堆栈结构和Java内存栈的抽象模型结构自己比较类似,因此咱们在学习过程就正常把这两种结构放在一块儿考虑了,并且两者除了概念上有一点点小的区别,理解成为一种结构对于初学者也何尝不可,因此实际上也能够以为两者没有太大的本质区别。可是在学习的时候最好分清楚内存堆栈和Java内存栈的一小点细微的差距,前者是物理概念和自己模型,后者是抽象概念和自己模型的一个共同体。而内存堆栈更多的说法能够理解为一个内存块,由于内存块能够经过索引和指针进行数据结构的组合,内存栈就是内存块针对数据结构的一种表示,而内存堆则是内存块的另一种数据结构的表示,这样理解更容易区份内存栈和内存堆栈(内存块)的概念。
  • 栈帧:栈帧是内存栈里面的最小单位,指的是内存栈里面每个最小内存存储单元,它针对内存栈仅仅作了两个操做:入栈和出栈,通常状况下:所说的堆栈帧和栈帧却是一个概念,因此在理解上记得加以区分
  • 内存堆:这里的内存堆和内存栈是相对应的,其实内存堆里面的数据也是存储在系统内存堆栈里面的,只是它使用了另一种方式来进行堆里面内存的管理,而本章题目要讲到的就是Java语言自己的内存堆和内存栈,而这两个概念都是抽象的概念模型,并且是相对的。
      栈帧:栈帧主要包括三个部分:局部变量、操做数栈帧(操做帧)和帧数据(数据帧)。本地变量和操做数帧的大小取决于须要,这些大小是在编译时就决定的,而且在每一个方法的类文件数据中进行分配,帧的数据大小则不同,它虽然也是在编译时就决定的可是它的大小和自己代码实现有关。当JVM调用一个Java方法的时候,它会检查类的数据来肯定在本地变量和操做方法要求的栈大小,它计算该方法所须要的内存大小,而后将这些数据分配好内存空间压入到内存堆栈中。
      栈帧——局部变量:局部变量是以Java栈帧组合成为的一个以零为基的数组,使用局部变量的时候使用的其实是一个包含了0的一个基于索引的数组结构。int类型、float、引用以及返回值都占据了一个数组中的局部变量的条目,而byte、short、char则在存储到局部变量的时候是先转化成为int再进行操做的,则long和double则是在这样一个数组里面使用了两个元素的空间大小,在局部变量里面存储基本数据类型的时候使用的就是这样的结构。举个例子:
class Example3a{
    public static int runClassMethod(int i,long l,float f,double d,Object o,byte b)
    {
        return 0;
    }
    public int runInstanceMethod(char c,double d,short s,boolean b)
    {
        return 0;
    }
}

Java内存模型-堆和栈

  栈帧——操做帧:和局部变量同样,操做帧也是一组有组织的数组的存储结构,可是和局部变量不同的是这个不是经过数组的索引访问的,而是直接进行的入栈和出栈的操做,当操做指令直接压入了操做栈帧事后,从栈帧里面出来的数据会直接在出栈的时候被读取和使用。除了程序计数器之外,操做帧也是能够直接被指令访问到的,JVM里面没有寄存器。处理操做帧的时候Java虚拟机是基于内存栈的而不是基于寄存器的,由于它在操做过程是直接对内存栈进行操做而不是针对寄存器进行操做。而JVM内部的指令也能够来源于其余地方好比紧接着操做符以及操做数的字节码流或者直接从常量池里面进行操做。JVM指令其实真正在操做过程的焦点是集中在内存栈栈帧的操做帧上的。JVM指令将操做帧做为一个工做空间,有许多指令都是从操做帧里面出栈读取的,对指令进行操做事后将操做帧的计算结果从新压入内存堆栈内。好比iadd指令将两个整数压入到操做帧里面,而后将两个操做数进行相加,相加的时候从内存栈里面读取两个操做数的值,而后进行运算,最后将运算结果从新存入到内存堆栈里面。举个简单的例子:
begin

iload_0 //将整数类型的局部变量0压入到内存栈里面
iload_1 //将整数类型的局部变量1压入到内存栈里面
iadd     //将两个变量出栈读取,而后进行相加操做,将结果从新压入栈中
istore_2 //将最终输出结果放在另一个局部变量里面

end

  综上所述,就是整个计算过程针对内存的一些操做内容,而总体的结构能够用下图来描述:
Java内存模型-堆和栈
  栈帧——数据帧:除了局部变量和操做帧之外,Java栈帧还包括了数据帧,用于支持常量池、普通的方法返回以及异常抛出等,这些数据都是存储在Java内存栈帧的数据帧中的。不少JVM的指令集实际上使用的都是常量池里面的一些条目,一些指令,只是把int、long、float、double或者String从常量池里面压入到Java栈帧的操做帧上边,一些指令使用常量池来管理类或者数组的实例化操做、字段的访问控制、或者方法的调用,其余的指令就用来决定常量池条目中记录的某一特定对象是否某一类或者常量池项中指定的接口。常量池会判断类型、字段、方法、类、接口、类字段以及引用是如何在JVM进行符号化描述,而这个过程由JVM自己进行对应的判断。这里就能够理解JVM如何来判断咱们一般说的:“原始变量存储在内存栈上,而引用的对象存储在内存堆上边。”除了常量池判断帧数据符号化描述特性之外,这些数据帧必须在JVM正常执行或者异常执行过程辅助它进行处理操做。若是一个方法是正常结束的,JVM必须恢复栈帧调用方法的数据帧,并且必须设置PC寄存器指向调用方法后边等待的指令完成该调用方法的位置。若是该方法存在返回值,JVM也必须将这个值压入到操做帧里面以提供给须要这些数据的方法进行调用。不只仅如此,数据帧也必须提供一个方法调用的异常表,当JVM在方法中抛出异常而非正常结束的时候,该异常表就用来存放异常信息。

  3)内存堆(Heap):

  当一个Java应用程序在运行的时候在程序中建立一个对象或者一个数组的时候,JVM会针对该对象和数组分配一个新的内存堆空间。可是在JVM实例内部,只存在一个内存堆实例,全部的依赖该JVM的Java应用程序都须要共享该堆实例,而Java应用程序自己在运行的时候它本身包含了一个由JVM虚拟机实例分配的本身的堆空间,而在应用程序启动的时候,任何一个Java应用程序都会获得JVM分配的堆空间,并且针对每个Java应用程序,这些运行Java应用程序的堆空间都是相互独立的。这里所说起到的共享堆实例是指JVM在初始化运行的时候总体堆空间只有一个,这个是Java语言平台直接从操做系统上可以拿到的总体堆空间,因此的依赖该JVM的程序均可以获得这些内存空间,可是针对每个独立的Java应用程序而言,这些堆空间是相互独立的,每个Java应用程序在运行最初都是依靠JVM来进行堆空间的分配的。即便是两个相同的Java应用程序,一旦在运行的时候处于不一样的操做系统进程(通常为java.exe)中,它们各自分配的堆空间都是独立的,不能相互访问,只是两个Java应用进程初始化拿到的堆空间来自JVM的分配,而JVM是从最初的内存堆实例里面分配出来的。在同一个Java应用程序里面若是出现了不一样的线程,则是能够共享每个Java应用程序拿到的内存堆空间的,这也是为何在开发多线程程序的时候,针对同一个Java应用程序必须考虑线程安全问题,由于在一个Java进程里面全部的线程是能够共享这个进程拿到的堆空间的数据的。可是Java内存堆有一个特性,就是JVM拥有针对新的对象分配内存的指令,可是它却不包含释放该内存空间的指令,固然开发过程能够在Java源代码中显示释放内存或者说在JVM字节码中进行显示的内存释放,可是JVM仅仅只是检测堆空间中是否有引用不可达(不能够引用)的对象,而后将接下来的操做交给垃圾回收器来处理。

  对象表示:

  JVM规范里面并无说起到Java对象如何在堆空间中表示和描述,对象表示能够理解为设计JVM的工程师在最初考虑到对象调用以及垃圾回收器针对对象的判断而独立的一种Java对象在内存中的存储结构,该结构是由设计最初考虑的。针对一个建立的类实例而言,它内部定义的实例变量以及它的超类以及一些相关的核心数据,是必须经过必定的途径进行该对象内部存储以及表示的。当开发过程给定了一个对象引用的时候,JVM必须可以经过这个引用快速从对象堆空间中去拿到该对象可以访问的数据内容。也就是说,堆空间内对象的存储结构必须为外围对象引用提供一种能够访问该对象以及控制该对象的接口使得引用可以顺利地调用该对象以及相关操做。所以,针对堆空间的对象,分配的内存中每每也包含了一些指向方法区的指针,由于从总体存储结构上讲,方法区彷佛存储了不少原子级别的内容,包括方法区内最原始最单一的一些变量:好比类字段、字段数据、类型数据等等。而JVM自己针对堆空间的管理存在两种设计结构:

  【1】设计一:

  堆空间的设计能够划分为两个部分:一个处理池和一个对象池,一个对象的引用能够拿处处理池的一个本地指针,而处理池主要分为两个部分:一个指向对象池里面的指针以及一个指向方法区的指针。这种结构的优点在于JVM在处理对象的时候,更加可以方便地组合堆碎片以使得全部的数据被更加方便地进行调用。当JVM须要将一个对象移动到对象池的时候,它仅仅须要更新该对象的指针到一个新的对象池的内存地址中就能够完成了,而后在处理池中针对该对象的内部结构进行相对应的处理工做。不过这样的方法也会出现一个缺点就是在处理一个对象的时候针对对象的访问须要提供两个不一样的指针,这一点可能很差理解,其实能够这样讲,真正在对象处理过程存在一个根据时间戳有区别的对象状态,而对象在移动、更新以及建立的整个过程当中,它的处理池里面老是包含了两个指针,一个指针是指向对象内容自己,一个指针是指向了方法区,由于一个完整的对外的对象是依靠这两部分被引用指针引用到的,而咱们开发过程是不可以操做处理池的两个指针的,只有引用指针咱们能够经过外围编程拿到。若是Java是按照这种设计进行对象存储,这里的引用指针就是平时说起到的“Java的引用”,只是JVM在引用指针还作了必定的封装,这种封装的规则是JVM自己设计的时候作的,它就经过这种结构在外围进行一次封装,好比Java引用不具有直接操做内存地址的能力就是该封装的一种限制规则。这种设计的结构图以下:
Java内存模型-堆和栈

  【2】设计二:

  另一种堆空间设计就是使用对象引用拿到的本地指针,将该指针直接指向绑定好的对象的实例数据,这些数据里面仅仅包含了一个指向方法区原子级别的数据去拿到该实例相关数据,这种状况下只须要引用一个指针来访问对象实例数据,可是这样的状况使得对象的移动以及对象的数据更新变得更加复杂。当JVM须要移动这些数据以及进行堆内存碎片的整理的时候,就必须直接更新该对象全部运行时的数据区,这种状况能够用下图进行表示:
Java内存模型-堆和栈
  JVM须要从一个对象引用来得到该引用可以引用的对象数据存在多个缘由,当一个程序试图将一个对象的引用转换成为另一个类型的时候,JVM就会检查两个引用指向的对象是否存在父子类关系,而且检查两个引用引用到的对象是否可以进行类型转换,并且全部这种类型的转换必须执行一样的一个操做:instanceof操做,在上边两种状况下,JVM都必需要去分析引用指向的对象内部的数据。当一个程序调用了一个实例方法的时候,JVM就必须进行动态绑定操做,它必须选择调用方法的引用类型,是一个基于类的方法调用仍是一个基于对象的方法调用,要作到这一点,它又要获取该对象的惟一引用才能够。无论对象的实现是使用什么方式来进行对象描述,都是在针对内存中关于该对象的方法表进行操做,由于使用这样的方式加快了实例针对方法的调用,并且在JVM内部实现的时候这样的机制使得其运行表现比较良好,因此方法表的设计在JVM总体结构中发挥了极其重要的做用。关于方法表的存在与否,在JVM规范里面没有严格说明,也有可能真正在实现过程只是一个抽象概念,物理层它根本不存在,针对放发表实现对于一个建立的实例而言,它自己具备不过高的内存须要求,若是该实现里面使用了方法表,则对象的方法表应该是能够很快被外围引用访问到的。
  有一种办法就是经过对象引用链接到方法表的时候,以下图:
Java内存模型-堆和栈
  该图代表,在每一个指针指向一个对象的时候,其实是使用的一个特殊的数据结构,这些特殊的结构包括几个部分:

  • 一个指向该对象类全部数据的指针
  • 该对象的方法表
      实际上从图中能够看出,方法表就是一个指针数组,它的每个元素包含了一个指针,针对每一个对象的方法均可以直接经过该指针在方法区中找到匹配的数据进行相关调用,而这些方法表须要包括的内容以下:
  • 方法内存堆栈段空间中操做栈的大小以及局部变量
  • 方法字节码
  • 一个方法的异常表
      这些信息使得JVM足够针对该方法进行调用,在调用过程,这种结构也可以方便子类对象的方法直接经过指针引用到父类的一些方法定义,也就是说指针在内存空间以内经过JVM自己的调用使得父类的一些方法表也能够一样的方式被调用,固然这种调用过程避免不了两个对象之间的类型检查,可是这样的方式就使得继承的实现变得更加简单,并且方法表提供的这些数据足够引用对对象进行带有任何OO特征的对象操做。
      另一种数据在上边的途中没有显示出来,也是从逻辑上讲内存堆中的对象的真实数据结构——对象的锁。这一点可能须要关联到JMM模型中讲的进行理解。JVM中的每个对象都是和一个锁(互斥)相关联的,这种结构使得该对象能够很容易支持多线程访问,并且该对象的对象锁一次只能被一个线程访问。当一个线程在运行的时候具备某个对象的锁的时候,仅仅只有这个线程能够访问该对象的实例变量,其余线程若是须要访问该实例的实例变量就必须等待这个线程将它占有的对象锁释放事后才可以正常访问,若是一个线程请求了一个被其余线程占有的对象锁,这个请求线程也必须等到该锁被释放事后才可以拿到这个对象的对象锁。一旦这个线程拥有了一个对象锁事后,它本身能够屡次向同一个锁发送对象的锁请求,可是若是它要使得被该线程锁住的对象能够被其余锁访问到的话就须要一样的释放锁的次数,好比线程A请求了对象B的对象锁三次,那么A将会一直占有B对象的对象锁,直到它将该对象锁释放了三次。
      不少对象也可能在整个生命周期都没有被对象锁锁住过,在这样的状况下对象锁相关的数据是不须要对象内部实现的,除非有线程向该对象请求了对象锁,不然这个对象就没有该对象锁的存储结构。因此上边的实现图能够知道,不少实现不包括指向对象锁的“锁数据”,锁数据的实现必需要等待某个线程向该对象发送了对象锁请求事后,并且是在第一次锁请求事后才会被实现。这个结构中,JVM却可以间接地经过一些办法针对对象的锁进行管理,好比把对象锁放在基于对象地址的搜索树上边。实现了锁结构的对象中,每个Java对象逻辑上都在内存中成为了一个等待集,这样就使得全部的线程在锁结构里面针对对象内部数据能够独立操做,等待集就使得每一个线程可以独立于其余线程去完成一个共同的设计目标以及程序执行的最终结果,这样就使得多线程的线程独享数据以及线程共享数据机制很容易实现。
      不只仅如此,针对内存堆对象还必须存在一个对象的镜像,该镜像的主要目的是提供给垃圾回收器进行监控操做,垃圾回收器是经过对象的状态来判断该对象是否被应用,一样它须要针对堆内的对象进行监控。而当监控过程垃圾回收器收到对象回收的事件触发的时候,虽然使用了不一样的垃圾回收算法,不论使用什么算法都须要经过独有的机制来判断对象目前处于哪一种状态,而后根据对象状态进行操做。开发过程程序员每每不会去仔细分析当一个对象引用设置成为null了事后虚拟机内部的操做,但实际上Java里面的引用每每不像咱们想像中那么简单,Java引用中的虚引用、弱引用就是使得Java引用在显示提交可回收状态的状况下对内存堆中的对象进行的反向监控,这些引用能够监视到垃圾回收器回收该对象的过程。垃圾回收器自己的实现也是须要内存堆中的对象可以提供相对应的数据的。其实这个位置到底JVM里面是否使用了完整的Java对象的镜像仍是使用的一个镜像索引我没有去仔细分析过,总之是在堆结构里面存在着堆内对象的一个相似拷贝的镜像机制,使得垃圾回收器可以顺利回收再也不被引用的对象。

      4)内存栈和内存堆的实现原理探测【该部分为不肯定概念】:

      实际上不管是内存栈结构、方法区仍是内存堆结构,归根到底使用的是操做系统的内存,操做系统的内存结构能够理解为内存块,经常使用的抽象方式就是一个内存堆栈,而JVM在OS上边安装了事后,就在启动Java程序的时候按照配置文件里面的内容向操做系统申请内存空间,该内存空间会按照JVM内部的方法提供相应的结构调整。
      内存栈应该是很容易理解的结构实现,通常状况下,内存栈是保持连续的,可是不绝对,内存栈申请到的地址实际上不少状况下都是连续的,而每一个地址的最小单位是按照计算机位来算的,该计算机位里面只有两种状态1和0,而内存栈的使用过程就是典型的相似C++里面的普通指针结构的使用过程,直接针对指针进行++或者--操做就修改了该指针针对内存的偏移量,而这些偏移量就使得该指针能够调用不一样的内存栈中的数据。至于针对内存栈发送的指令就是常见的计算机指令,而这些指令就使得该指针针对内存栈的栈帧进行指令发送,好比发送操做指令、变量读取等等,直接就使得内存栈的调用变得更加简单,并且栈帧在接受了该数据事后就知道到底针对栈帧内部的哪个部分进行调用,是操做帧、数据帧仍是局部变量。
      内存堆实际上在操做系统里面使用了双向链表的数据结构,双向链表的结构使得即便内存堆不具备连续性,每个堆空间里面的链表也能够进入下一个堆空间,而操做系统自己在整理内存堆的时候会作一些简单的操做,而后经过每个内存堆的双向链表就使得内存堆更加方便。并且堆空间不须要有序,甚至说有序不影响堆空间的存储结构,由于它归根究竟是在内存块上边进行实现的,内存块自己是一个堆栈结构,只是该内存堆栈里面的块如何分配不禁JVM决定,是由操做系统已经最开始分配好了,也就是最小存储单位。而后JVM拿到从操做系统申请的堆空间事后,先进行初始化操做,而后就能够直接使用了。
      常见的对程序有影响的内存问题主要是两种:溢出和内存泄漏,上边已经讲过了内存泄漏,其实从内存的结构分析,泄漏这种状况很难甚至说不可能发生在栈空间里面,其主要缘由是栈空间自己很难出现悬停的内存,由于栈空间的存储结构有多是内存的一个地址数组,因此在访问栈空间的时候使用的都是索引或者下标或者就是最原始的出栈和入栈的操做,这些操做使得栈里面很难出现像堆空间同样的内存悬停(也就是引用悬挂)问题。堆空间悬停的内存是由于栈中存放的引用的变化,其实引用能够理解为从栈到堆的一个指针,当该指针发生变化的时候,堆内存碎片就有可能产生,而这种状况下在原始语言里面就常常发生内存泄漏的状况,由于这些悬停的堆空间在系统里面是不可以被任何本地指针引用到,就使得这些对象在未被回收的时候脱离了可操做区域而且占用了系统资源。
      栈溢出问题一直都是计算机领域里面的一个安全性问题,这里不作深刻讨论,说多了就偏离主题了,而内存泄漏是程序员最容易理解的内存问题,还有一个问题来自于我一个***朋友就是:堆溢出现象,这种现象可能更加复杂。
      其实Java里面的内存结构,最初看来就是堆和栈的结合,实际上能够这样理解,实际上对象的实际内容才存在对象池里面,而有关对象的其余东西有可能会存储于方法区,而平时使用的时候的引用是存在内存栈上的,这样就更加容易理解它内部的结构,不只仅如此,有时候还须要考虑到Java里面的一些字段和属性究竟是对象域的仍是类域的,这个也是一个比较复杂的问题。

      两者的区别简单总结一下:

  • 管理方式:JVM本身能够针对内存栈进行管理操做,并且该内存空间的释放是编译器就能够操做的内容,而堆空间在Java中JVM自己执行引擎不会对其进行释放操做,而是让垃圾回收器进行自动回收
  • 空间大小:通常状况下栈空间相对于堆空间而言比较小,这是由栈空间里面存储的数据以及自己须要的数据特性决定的,而堆空间在JVM堆实例进行分配的时候通常大小都比较大,由于堆空间在一个Java程序中须要存储太多的Java对象数据
  • 碎片相关:针对堆空间而言,即便垃圾回收器可以进行自动堆内存回收,可是堆空间的活动量相对栈空间而言比较大,颇有可能存在长期的堆空间分配和释放操做,并且垃圾回收器不是实时的,它有可能使得堆空间的内存碎片主键累积起来。针对栈空间而言,由于它自己就是一个堆栈的数据结构,它的操做都是一一对应的,并且每个最小单位的结构栈帧和堆空间内复杂的内存结构不同,因此它通常在使用过程不多出现内存碎片。
  • 分配方式:通常状况下,栈空间有两种分配方式:静态分配和动态分配,静态分配是自己由编译器分配好了,而动态分配可能根据状况有所不一样,而堆空间倒是彻底的动态分配的,是一个运行时级别的内存分配。而栈空间分配的内存不须要咱们考虑释放问题,而堆空间即便在有垃圾回收器的前提下仍是要考虑其释放问题。
  • 效率:由于内存块自己的排列就是一个典型的堆栈结构,因此栈空间的效率天然比起堆空间要高不少,并且计算机底层内存空间自己就使用了最基础的堆栈结构使得栈空间和底层结构更加符合,它的操做也变得简单就是最简单的两个指令:入栈和出栈;栈空间针对堆空间而言的弱点是灵活程度不够,特别是在动态管理的时候。而堆空间最大的优点在于动态分配,由于它在计算机底层实现多是一个双向链表结构,因此它在管理的时候操做比栈空间复杂不少,天然它的灵活度就高了,可是这样的设计也使得堆空间的效率不如栈空间,并且低不少。
相关文章
相关标签/搜索