jvm 虚拟机内存模型

来源:https://blog.csdn.net/A_zhenzhen/article/details/77917991?locationNum=8&fps=1

     https://blog.csdn.net/hxpjava1/article/details/55189077

概述

  java的内存管理采用自动内存管理机制,这样就不需要程序员去写释放内存的代码,而且不容易出现内存泄漏问题。正是由于内存的申请和释放都交给了Java虚拟机,一旦出现内存泄漏和溢出问题时,在不了解Java虚拟机内存结构和自动管理机制的情况下,很难排查问题的所在。所以一个成熟的程序员和架构师,必须很好的掌握Java虚拟机的自动内存管理机制。

运行时数据区

  jvm 虚拟机内存模型

上图的虚拟机运行时数据区是Java虚拟机规范所规定的区域,不同的虚拟机有不同的实现

Java虚拟机栈 / 本地方法栈

  Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟C语言的数据段中的栈类似。事实上,Java栈是Java方法执行的内存模型。为什么这么说呢?下面就来解释一下其中的原因。

  Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。下图表示了一个Java栈的模型:

jvm 虚拟机内存模型

  局部变量表,顾名思义,想必不用解释大家应该明白它的作用了吧。就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

  操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

  指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

  方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

  由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰

  本地方法栈和虚拟机栈非常相似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。会抛出*Error和OOM异常。

  本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以*实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。

程序计数器 

  程序计数器(Program Counter Register),也有称作为PC寄存器。想必学过汇编语言的朋友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。

  虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示 执行哪条指令的。

  由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。

  在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。

  由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

  Java堆用于存放对象实例:The heap is the runtime data area from which momory which memory for all class instances and arrays is allocated。是垃圾收集器管理的主要区域。可细分为:新生代和老年代;新生代又可分为Eden,from Survivor,to Survivor。会抛出*Error异常。

方法区  

  方法区存储虚拟机加载的类信息常量静态变量,即时编译器编译后的代码等数据。HotSpot中也称为永久代(Permanent Generation),(存储的是除了Java应用程序创建的对象之外,HotSpot虚拟机创建和使用的对象)。为什么称为永久代呢?? 各个地方说的都不清楚,查看官方文档,解释为:永久代中的对象并不是永久的,只是历史上被叫做永久代罢了。 In fact, the objects in it are not “permanent”, but that's what it has been called historically.
  方法区在不同虚拟机中有不同的实现,HotSpot在1.7版本以前和1.7版本,1.7后都有变化。

jdk7版本以前的实现

  jvm 虚拟机内存模型

jdk7版本的改动是把字符串常量池移到了堆中

jdk8 MetaSpace jdk1.8中则把永久代给完全删除了,取而代之的是 MetaSpace

jvm 虚拟机内存模型

运行时常量池和静态变量都存储到了堆中,MetaSpace存储类的元数据,MetaSpace直接申请在本地内存中(Native memory),这样类的元数据分配只受本地内存大小的限制,OOM问题就不存在了。除此之外,还有其他很多好处:
  • Take advantage of Java Language Specification property : Classes and associated metadata lifetimes match class loader’s
  • Linear allocation only
  • No individual reclamation (except for RedefineClasses and class loading failure)
  • No GC scan or compaction
  • No relocation for metaspace objects

运行时常量池

  运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中存储有常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。一般来说,处理保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
  运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性。Java语言并不要求常量一定只有编译期才能产生。

对象的内存布局

jvm 虚拟机内存模型

Mark Word用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
类型指针即对象指向它的类元数据的指针。并不是所有的虚拟机实现都必须在对象数据上保留类型指针(用句柄实现)。
实例数据  存储程序代码中定义的各种类型的字段内容,这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义的顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,oop(Ordinary Object Pointers)。
对齐填充 并不是必然存在的,没用特别的含义。HotSpot的自动内存管理系统要求对象的起始地址必须是8字节的整数倍(对象的大小必须是8字节的整数倍)。

对象的创建

  虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查符号引用代表的类是否已被加载,解析和初始化过。如果没有就先执行类的加载过程。接下来虚拟机为新对象分配内存(指针碰撞或空闲列表,Serial,ParNew等带Compact过程的收集器时采用指针碰撞,CMS这种基于Mark-Sweep缩放的收集器时通常采用空闲列表)。
处理并发是通过CAS配上失败重试的方式或者每个线程在堆上预先分配本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。
  内存分配完成后,虚拟机将内存空间都初始化为零值(不包括对象头)。然后对对象头数据进行设置。
  在完成以上工作后,从虚拟机的视角来看,一个新的对象已经产生。但从Java程序的视角来看,在执行完new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的访问定位

jvm 虚拟机内存模型

jvm 虚拟机内存模型

  这两种对象的访问方式各有优势,使用句柄来访问的最大好吃就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改
使用直接指针访问的最大好处就是速度快,它节省了异常指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多也是一项可观的执行成本。
HotSpot 是通过直接指针访问对象的方式进行对象访问的。
 
辅助学习还可以看这篇  Java内存模型
上一篇:sqlserver 通过日志恢复误删的数据


下一篇:JVM:内存模型