JVM内存区域(二)—— HotSpot虚拟机分析

版权声明:欢迎评论和转载,转载时请注明作者和出处,共同维护良好的版权和知识产权秩序。 https://blog.csdn.net/CrazyOnes/article/details/82954667

上一节我们讲了Java虚拟机的理论内存模型,同时我们也提到了,这些只是Java虚拟机规范中的内容,如果我们要研究一个对象是如何创建、如何布局等一系列细节问题的时候,我们就必须在具体的虚拟机中分析,因为不同的虚拟机的实现是不一样的,下面我们就选最常用、最普遍的虚拟机——HotSpot来介绍。

对象的创建

Java是一门面向对象语言,在创建对象的时候仅适用关键字new即可创造一个对象。接下来我们就说一下使用new关键字之后,内存中究竟发生了什么变化。

虚拟机读到new指令之后,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个类是否执行过类加载过程,如果没有加载过,则先执行类加载。

类加载检查完成后,虚拟机将会为新对象分配内存。对象所需的内存大小在类加载完成之后便可以确定。下边的任务便是Java堆中划分出一块规整的内存。但是Java堆采用的垃圾收集算法的不同,给新对象分配内存的方式也不同:

  1. 当垃圾收集算法使用复制算法、标记-整理算法时,Java堆中的内存是绝对规整的,即所有被使用的内存放在一侧,未被使用的内存放在另一侧,中间放着一个指针作为分界点的指示器,分配内存仅仅是把指针向空闲空间挪动一个新对象大小的距离。这种方式称为“指针碰撞”。
  2. 当垃圾收集算法使用标记-清楚算法时,Java堆中的内存不是规整的。空闲内存与非空闲内存相互交错。这时虚拟机维护一个内存列表,记录哪些内存块是可用的,再分配内存的时候从列表中找到一块足够大的空间划分给新对象,并更新列表上的记录。这种分配方式称为“空闲列表”。

在为新对象分配空间的时候还需要考虑一个问题,因为虚拟机中对象的创建时十分频繁的,如果使用指针碰撞法,那么必须要考虑到在并发的情况下是否线程安全的问题。虚拟机给出的解决方案有两个:

  • 对分配内存空间的动作进行同步处理,在虚拟机实际实现中采用CAS配上失败重试的方式保证操作更新的原子性。
  • 把内存的分配按照线程划分在不同空间之中进行,这样每个线程预先在Java堆中分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上进行分配,只有TLAB用完需要分配新的TLAB的时候,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

内存分配完成之后,虚拟机需要将分配到的内存空间初始化为零值(对象头除外,关于对象内存区域的组成在对象内存布局中还会讲到),如果使用TLAB,这一过程也可以在TLAB分配时进行。紧接着对对象头进行设置。到这一步的时候,从虚拟机的角度来说,一个新对象已经产生了,但是从程序或者程序员角度来看,对象才刚刚创建,此时还没有执行对象的构造方法,所有的字段还都是零。一般来说这一步之后紧接着就会执行构造方法,将对象按照程序员的意愿初始化,这样才是一个真正可用的对象。

对象内存的布局

先来看一张图:

对象的内存布局
对象的内存布局

我们可以很直观的看出来,对象存储分为三块区域:对象头、实例数据和对齐填充。

对象头

对象头有两部分信息:

第一部分用于存储对象自身的运行时数据,比如哈希码、GC分代年龄(用于垃圾回收)、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度取决于虚拟机的位数,在32位和64位虚拟机中长度分别为32bit和64bit。

第二部分用于存储类型指针,也就是对象指向它的类元数据的指针。虚拟机通过这个指针确定对象是哪个类的实例。但是并不是所有的虚拟机实现都必须有类型指针,这取决于引用类型访问对象的方式。目前有使用句柄和直接指针两种方式来访问对象:

  • 如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图所示:
    通过句柄访问对象
    通过句柄访问对象
  • 如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何防止类型数据的相关信息,而引用中存储的直接就是对象地址,如图所示:
    通过直接指针访问对象
    通过直接指针访问对象
     

这两种访问方法各有优势,使用句柄来访问对象最大的好处就是引用中存储的数据比较稳定,因为如果发生垃圾回收,变化的只是句柄中指向对象实例数据的指针,引用的值本身不用修改。使用直接指针最大的好处就是,速度比较快,省去了一次指针定位的时间开销。由于对象的访问在Java中十分频繁,所以也是一项非常可观的开销节约。HotSpot虚拟机使用直接指针的方式访问对象。

实例数据

实例数据部分是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是子类中定义的,都需要记录起来。字段的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)。从分配策略中可以看出相同宽度的字段总是分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前,但是如果设置CompactFields参数值为true(默认为true),那么子类中较窄的变量也可能会插入到父类的空隙中去。

对齐填充

对其填充不是必然存在的,仅起到占位符的作用。因为HotSpot虚拟机规定自动内存管理系统要求对象其实地址必须是8字节的整数倍,换句话说,就是对象的大小是8字节的整数倍,如果不满足条件,则需要对齐填充来补全。

结语

到这里Java内存模型关于HotSpot虚拟机的实例分析就完成了,这一部分内容还是很多的,仔细的、深入的研究之后才能更好的认识虚拟机,使用Java这门语言。 与君共勉。

上一节:JVM内存区域(一)—— 理论模型


如有错误,欢迎指摘。

猜你喜欢

转载自blog.csdn.net/CrazyOnes/article/details/82954667