JVM内存模型详解

内存模型

内存模型如下图所示

堆是Java虚拟机所管理的内存最大一块。堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一的目的就是存放对象实例。所有的对象实例都在这里分配内存

Java堆是垃圾收集器管理的主要区域。从内存回收的角度来看,由于现在的垃圾收集器采用的是分代收集算法。所以,java堆又分为新生代老年代。从内存分配的角度来说,线程共享的java对中可能划分出多个线程私有的fenp缓冲区(Thread Local Allocation Buffer)。

可以通过 -Xms-Xmx分别控制堆初始化是最小堆内存和最大堆内存大小。

虚拟机栈

与程序计数器一样,java虚拟机栈也是线程私有的,他的生命周期与线程相同

虚拟机栈描述的是Java方法的执行的内存模型:每个方法在执行的同时会创建一个栈桢(stack frame)用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着栈桢在虚拟机栈中入栈到出栈的过程。

虚拟机栈存储的数据类型
  • 局部变量表

    存放的是编译器可知得到各种基本数据类型boolean、byte、char、short、int、float、long、double、对象引用(refrence类型,不等同于对象本身,一个指向对象的起始内存位置的引用指针)
  • 操作数栈
  • 动态链表
  • 方法出口
  • ...
常见异常

在虚拟机规范中,对这个区域规定了两种异常情况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError
  2. 如果虚拟机栈可以动态扩展,扩展时无法申请做够的内存,将会爬出OutOfMemorryError

本地方法栈

与虚拟机栈发挥的作用非常类似,他们之间的区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。与虚拟机栈一样,本地房发展区域也会抛出StackOverflowErrorOutOfMemorryError异常。

方法区(1.8后该区域被废弃)

方法区与java堆一样,是各个线程所共享的,它用来存储已被虚拟机加载的类信息常量静态变量即时编译后的代码等数据

方法区是jvm提出的规范,而永久代就是方法区的具体实现。

java虚拟机对方法区的限制非常宽松,可以像堆一样不需要连续的内存可可选择的固定大小外,还可以选择不识闲垃圾收集,相对而言,垃圾收集行为在这边区域是比较少出现的。

在方法区会报出 永久代内存溢出的错误。而java1.8为了解决这个问题,就提出了meta space(元空间)的概念,就是为了解决永久代内存溢出的情况,一般来说,在不指定 meta space大小的情况下,虚拟机方法区内存大小就是宿主主机的内存大小

程序计数器

程序计数器是一块较小的内存空间,他可以看做是当前线程所执行字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选择下一条将要执行的字节码指令。

由于JAVA虚拟机的多线程是通过多线程流转切换并分配处理器执行时间的方式来实现的。在任一一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程的计数器之间互不影响,独立存储,我们称该类内存区域为线程私有

如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。

运行时常量池

运行时常量池是方法区的一部分。Class文件除了 有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性.Java语言并不要求常量一定只有在编译器才能产生,依旧是并非预置入Class文件中的常量池的内容才能进入方法区运行时常量池

对象创建

在语言层面上,创建对象(克隆,反序列化)通常只是一个new关键字。

过程

虚拟机在遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到这个类的符号引用并且减产这个符号引用代表的类是否已经被加载、解析、和初始化国。如果没有,那必须执行相应的类加载过程。

在类加载检查通过后,接下来虚拟机将为新生的对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。

内存分配

为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。假设java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的放在另外一边,中间放着一个指针最为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲的空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞

如果内存不是规整的,已使用的和空闲的内存区域是相互交错的,虚拟机必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新这个列表。这种分配方式是空闲列表

选择哪种分配分配方式是由java堆是否规整来决定的,而java堆是否规整又是由其所采用 的垃圾收集器是否带有压缩规整的功能决定,因此,使用SerialParNew等带来Compat过程的收集器时,分配算法是指针碰撞。而使用CMS这种基于Mark-Swaeep算法,采用的是空闲列表分配方式。

对象的内存布局

在HotSpot虚拟机中,对象在内存中的存储布局分为3块区域:对象头(Header)实例数据(Instance Data)对其补充(Padding)

对象头(Heading)

对象头包括两部分信息

  1. 用于存储对象自身运行时数据。

如哈希码,GC分代年龄、锁状态标志、偏向线程ID。这部分s数据的长度在32位和64位的虚拟机中分别为32bit和64bit。

对象的访问定位

创建对象时为了使用对象,java程序需要通过栈上的refrence数据来操作堆上的具体对象。由于refrence类在java虚拟机值规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的具体位置,所以对象访问方式也取决于虚拟机对这个refrence的具体实现,目前主流的访问凡是是使用句柄直接指针两种。

句柄访问

如果使用句柄访问的话,那么java堆中就会划分出一块内存来座位句柄池,refrence中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据的各自具体的地址信息。

指针直接访问

如果使用指针直接访问,那么java堆对象的布局就必须考虑如何放置访问类型数据的相关信息,而refrence中存储的直接就是对象地址


这个两种访问方式各有优势,使用句柄访问的最大好处就是refrence中存储的是稳定的句柄地址,在对象被移动(垃圾收集时)只会改变句柄中对象实例指针,refrence本省不需要修改。

使用直接指针访问的方式最大的好处就是速度更快,节省了一次指针定位的事件开销。由于对象的访问在java中非常频繁,一次这类开销积少成多也是一个比较客观的优化。

posted @ 2019-05-26 12:32  Ronaldo7  阅读(18315)  评论(1编辑  收藏  举报