深入JVM之理解JVM内存区域与对象创建内存布局

Posted 路过你的全世界

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入JVM之理解JVM内存区域与对象创建内存布局相关的知识,希望对你有一定的参考价值。

  • 写在前面
    java语言通过IDE的编译生成class文件,然后java虚拟机加载class文件到内存,之后运行在java虚拟机上。在这样的一个宏观的过程中,JVM的内存分区到底是什么样的呢?他们的作用又是什么呢?

JVM运行时数据区

首先先从整体来看下JVM分区大体情况:

程序计数器

程序计数器是一块比较小的内存区域,是当前线程所执行的字节码的行号指示器。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

java虚拟机的多线程是通过线程轮转(线程切换)来实现的。所以每个线程在被切换之后需要保存、更新线程的执行进度,程序计数器就派上了用场。

此区域是唯一一个不会产生OOM的内存区域。其他区域都会有OOM的危险。

虚拟机栈

虚拟机栈属于线程私有,主要存放java方法,属于java方法执行的内存模型。每个方法在执行的时候都会产生一个栈帧,在栈帧里面存放着该方法的局部变量表、操作数栈等信息。每个方法执行到结束的过程也就是对应栈帧在虚拟机栈中入栈与出栈的过程。

我们平时讲的“栈”其实严格意义上说的就是虚拟机栈,具体上就是特指栈帧中的局部变量表了。平时讲的“堆”指的就是java堆。

该区域如果内存不够用就会造成OOM。

本地方法栈

本地方法栈对应虚拟机栈,同样也是线程私有。虚拟机栈为java方法服务,而本地方法栈对应的是Native方法,为native方法服务。

java堆

java堆是java虚拟机所管理的内存中最大的一份。java堆是被所有的线程共享的一块内存空间。它的主要作用就是存放对象实例,基本上所有的对象实例都在这里分配内存空间。同时这个内存区域也是垃圾回收机制着重清理的区域。

从内存回收角度来看,现在垃圾收集器主流技术是使用分代收集算法,这样java堆可以分为新生代(例如:“朝生夕死”的对象实例)与老年代(大对象、经历过数次GC仍然存活的对象)。再细分还有Eden空间、From Survivor空间等等。

java堆可以位于物理上不连续的内存空间中,只要求它们在逻辑上连续就可以。当然如果堆中没有了内存可以分配给对象实例,那么就会造成OOM。

直接内存

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

在JDK 1.4 中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配内存,然后通过java堆上的一个DirectByteBuffer对象作为该内存区域的一个引用进行操作。这样可以避免在java堆中和Native堆中来回复制数据,提高性能。

方法区

方法区属于各个线程共有的内存区域,主要用于存储已经被虚拟机加载的类的信息、常量、静态变量等数据。垃圾收集器在这个区域比较少出现。一度还出现方法区被叫做“永久代”。但是事实上并不是这样。

垃圾回收器也会清理方法区,主要是从两个方面:一个是清除废弃的常量;另外一个是清除不再使用的类。关于垃圾回收机制在这里不再展开讲解,接下来会有专门的一篇博客讲解JVM垃圾回收机制。

运行时常量池

运行时常量区属于方法区的一部分,class文件(默认对应一个类或者一个接口)中包含了一个类或者一个接口的所有信息:类的版本、字段值、访问标志。方法。接口等等信息,还有一项就是常量池。我们将常量池单独提出来就是因为这部分被提到的次数较多,然后做下基本介绍。

class文件中的常量池再被java虚拟机加载之后,这部分内容会放到方法区中的运行时常量池存放。运行时常量池相对于class文件中的常量池最大的一个特点就是具备“动态性”。java语言并没有要求常量一定要在编译器才能产生,也就是说并非预置入class文件中常量池的内容才进入方法区中的运行时常量池。运行期间的常量也可以进入。这样的情况我们开发人员利用的比较多的就是使用String的intern()方法。


对象创建与内存布局

我们在编写java代码的时候,经常要实例化某一个对象,那么就是使用new来创建一个对象。那么,在jvm层次来看,他到底是怎样进行的呢?

对象创建

当虚拟机遇到一条new指令时候,它会首先检查该类有没有被加载、解析和初始化,如果没有的话,那么就要进行类的加载操作。类加载完成之后就是虚拟机为新生对象分配内存。对象所需要的内存在类加载完成之后就被完全确定。内存分配的过程其实就是将java堆中的一部分内存画出给这个对象的过程。具体的方式有两种(选取哪种方式就要看java堆是否规整,其实也就是看使用的对应垃圾收集器是否具有内存整理压缩功能):

  • 指针碰撞

    如果java堆中的内存是规整的,使用的内存在一边,空闲的内存在另外一部分,那么中间肯定有一个指针作为分界点的指示器。那么分配内存的过程就是指针移动的过程,只要移动相应的距离即可。这样的方式叫做“指针碰撞”

  • 空闲列表

    如果内存空间并不规整,使用过得内存和空闲内存没有严格区分开来。那么我们就需要一个表来记录哪些内存块还可以使用,再分配的时候从列表中找到足够的大的一块内存来存放对象实例,并且要更新表上的记录。这样的分配方式称之为“空闲列表”。

内存分配完成之后就是对分配好的内存空间进行赋零值(java虚拟机会自动的给各个不同的类型赋值对应的零值)

第二步就是虚拟机对对象进行必要的配置,例如这个对象是哪个类的实例、如何找到这个类的信息、对象的哈希码等等,这些信息保存在对象的对象头中。

第三步就是对这个对象进行实例化了。我们自己的java代码中对对象字段的赋值等等操作就是在这一步完成。

对象内存布局

上一部分我们讲到了对象的对象头,对象头是什么东西呢?下面我们讲解下对象在内存中的布局。以HotSpot虚拟机为例,对象在内存中的布局可以分为三部分:对象头、实例数据、对齐填充。

  • 对象头
    对象头包括两部分:第一部分用于存储对象自身的运行时数据:哈希码、GC分代、线程持有锁等信息;另一部分就是指类型指针,就是对象指向它的类元数据的指针,虚拟机通过这个指针来判断这个对象属于哪个类的实例。

  • 实例数据
    该部分就是对象真正存储的有效数据区域,也就是我们在代码里面所定义的各种类型的字段内容。无论从父类继承下来的还是在子类中定义的,都需要记录下来。

  • 对齐填充
    非必须存在,主要用于内存数据对齐。

对象访问定位

对象创建在java堆上,我们如何才能访问他们呢?其实,java程序需要通过栈上的一个reference字段数据来操作堆上的具体对象,即栈上的reference字段指向堆上的具体对象。目前主流的访问方式有两种:

  • 使用句柄
    如果使用句柄进行访问,那么java堆会划出一部分内存来作为句柄池,reference存储的对象就是句柄池中的句柄地址。句柄中包含了对象实例数据与类型数据各自的具体地址信息。

  • 直接访问

    如果是直接访问,那么java堆中就要包含一个指向方法区的该对象类型数据的指针(引用),而reference中包含的就直接是对象地址。

以上的这两种方式来说,使用句柄的好处就是reference中存储的是一个稳定的地址,在对象被移动的过程中,只会更新句柄中的实例数据指针,并不会改变reference中的数据;使用直接访问方式的话,它的访问速度就是更快。因为减少了一次指针的定位,这样节省了时间。因为对象的访问在java堆中十分频繁,所以这样的节省操作一直积累也会成为一个很可观的减少时间成本的操作。

以上是关于深入JVM之理解JVM内存区域与对象创建内存布局的主要内容,如果未能解决你的问题,请参考以下文章

深入理解JVM之JVM内存区域与内存分配

深入理解JVM之JVM内存区域与内存分配

深入理解JVM之JVM内存区域与内存分配

深入理解JVM之内存区域

深入理解Java虚拟机:JVM内存管理与垃圾收集理论

深入理解JVM之知识体系