在大概了解了Java虚拟机中内存的大致分布后,接下来就应该了解虚拟机是如何在内存中管理对象的,毕竟Java是一门面向对象的语言,在Java程序的运行过程中会不断有对象创建出来。为了方便,这里仅仅以HotSpot虚拟机和Java堆内存为例,介绍下HotSpot虚拟机在Java堆中对象分配、布局和访问的过程。

1、对象的创建

在Java语言中,我们可以使用new关键字创建一个对象(这里仅仅讨论普通的Java对象,不包括数组和Class对象等),在虚拟机中,创建对象的过程比我们想象的要复杂一些。

在虚拟机创建对象之前会加载类,这部分也比较重要,这里先略过这部分,假设要创建的对象已经加载成功。我们从虚拟机的角度来考虑,如果要创建一个对象,要考虑这几个问题:

  • 创建对象就要内存,在哪里分配内存?
  • 有了可以分配内存的地方,然后内存如何分配呢?
  • 实际中对象创建非常频繁,如何保证分配内存时的线程安全?
  • 分配完内存后如何设置内存要存储的内容?

首先,Java堆是线程共享的,大多数对象都会在这里创建,所以虚拟机也会在这里创建对象。但是Java堆是一块内存区域,在从这块内存中分出一块可用的空间来创建对象时有两种分配方式:指针碰撞和空闲列表。这两种分配方式基于堆内存是否是规整的来选择的。如果Java堆中的内存绝对规整,所有用过的内存在一边,所有没用过的内存在一边,那么就可以维护一个作为中间分界线的指针,需要分配内存时,只需要将分界线向空闲方向移动相应的距离即可。

如果Java堆中的内存不是规整的,已经使用的内存和没有使用的内存相互交错,那么就需要虚拟机维护一个未被使用的空间的列表,即空闲列表,这里记录哪些内存是可用的,分配的时候找出一块够用的空间进行佩芬,并在分配后更新这个列表。

那么如何选择这两种方式呢?看起来是由Java堆是否规整决定,但Java堆是否规整又是由所采用的垃圾收集器是否带有压缩整理功能决定的。这就涉及到了Java垃圾回收机制,这里不过多介绍。

虽然Java中没有指针,但是虚拟机在内存中还是会使用指针的。就是说,分配一块内存后,用一个指针表示这块内存。这样,如果当多个线程同时创建对象时,可能会出现这样的问题:正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。

对于这个问题,有两个解决方法。实际上虚拟机采用CAS加上失败重试的方式保证更新操作的原子性;另一种是使用本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。TLAB是每个线程在Java堆中预先分配的一小块内存,哪个线程要分配内存,就在那个线程的TLAB上分配,只有TLAB上不够并分配新的TLAB时才需要同步。

内存分配后,虚拟机将分配的内存空间都初始化为零值(不包括对象头,对象头在后面介绍)。如果使用TLAB,那这个工作就在TLAB分配时进行。这样就保证了对象的实例字段在Java代码中可以不赋初值就直接使用,程序能访问到这些字段的数据类型对应的零值。

之后,虚拟机要对对象进行必要的设置,比如这个对象是哪个类的实例、如何才能找到类的元数据、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。

这是,在虚拟机看来,一个新的对象就创建完成了,不过,对象还没有初始化,所有的实例字段还都是零值,即对于Java程序来说,对象的创建才刚开始。然后执行对象的构造函数,将对象按照类的构造函数所期望的进行初始化,这样,一个对象就创建完了。

2、对象的内存布局

上面介绍了对象是如何创建的,那么分配的那块内存到底存了什么呢?

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块:对象头(Header)、实例数据(Instance Data)和对齐数据(Padding)。

对象头包含两部分,第一部分用于存储对象自身的运行时数据,和哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向锁ID、偏向时间戳等,这部分数据的长度在32位和64为位的虚拟机中分别是32位和64位,官方叫“Mark Word”。Mark Word结构如下:

由于对象要存储的运行时数据很多,已经超过了限制的长度,Mark Word被设计成了非固定的数据结构,以便在极小的空间内存储更多的数据,它会根据对象的状态复用自己的存储空间,如上图。

对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机可以通过这个指针来确定这个对象属于哪个类。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,也就是说查找对象的元数据信息不一定要经过对象本身。另外,如果对象是一个Java数组,那么对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以根据普通Java对象的元数据信息确定对象的大小,但是从数组的元数据中无法确定数组的大小。

接下来的数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段的内容。无论是从父类继承来的,还是子类中定义的,都要记录下来。这部分的存储顺序会受到虚拟机分配策略参数和字段在类中定义的顺序的影响。HotSpot虚拟机默认的分配策略是longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),可以看出,相同宽度的字段总是分配到一起。在这个情况下,父类的字段会在子类之前。

第三部分是填充数据,这部分不是必须的,仅仅起到占位符的作用。HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,如果不够的话,就需要填充来补齐。

3、对象的访问定位

上面的两部分解决了如何分配内存以及在内存中存放什么数据的问题。经过这两个步骤就创建好了一个Java对象,但创建对象是为了使用的。在Java虚拟机栈中有局部变量表,用来存储一个方法要用到的局部数据。对于基本类型的数据可以直接存放数据,但是对于对象实例就不可以了,因为对象种类太多也不能确定大小。这时可以用reference引用类型表示一个对象实例,这个reference指向Java堆中创建好的对象,就可以使用了。

不过,这个Java堆中的数据要使用时还需要知道所属的类的元数据信息,比如这个对象是属于哪个类的。这时,如何确定对象的类型数据就有两个方法:使用句柄访问和使用直接指针访问。

如果使用句柄访问,那么Java堆中就会划出一块内存来作为句柄池,reference中存放的就是对象的句柄地址,而句柄中包括了对象实例数据与类型数据各自的具体地址信息,如下图:

这样,reference就可以找到实例数据和类型数据了。

如果使用直接指针访问的话,那么Java堆对象的布局就需要存放类型数据的相关信息了,而reference中存放的就是对象地址,如下图:

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的就是稳定的句柄地址,在对象被移动(垃圾回收中这种情况经常发生)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

使用直接指针访问的方式的最大好处就是速度更快,因为它节省了一次指针定位的时间开销,由于Java中对象的访问非常频繁,这样的积累还是有很客观的性能提升的。而HotSpot中就是使用的这种访问方式。

添加公众号Machairodus,我会不时分享一些平时学到的东西~

HotSpot虚拟机在Java堆中对对象的管理相关推荐

  1. HotSpot虚拟机在java堆中的内存使用

    1  简介 依托JavaTM 2平台的力量,标准版(J2SETM)实现了内存的自动管理,将开发人员从复杂的显式内存管理中解放出来. 本文将对Sun公司的J2SE发行版中的Java HotSpot虚拟机 ...

  2. Class对象存储在Java堆中

    在JDK1.8完全废除永久代之前的JDK版本中,方法区是一个逻辑分区,实际是java堆的一部分,但是有Non-heap的标记,以便区分. 众所周知, java中new处的对象存放在java堆中,而对象 ...

  3. java堆中的组成部分,初识Java虚拟机的基本结构 | If Coding

    Java虚拟机是什么?是做什么的?可能这些问题在我们学习Java之初就伴随这我们. 一般来说我们使用IDE将我们编写好的Java程序,点击运行,在不出错的情况下,就会得到我们想要的结果.那么这期间到底 ...

  4. Java 虚拟机:Java 内存区域及对象,java 反射面试

    1.计算机存储单位 从小到大依次为位 Bit.字节 Byte.千字节 KB.兆 M.千兆 GB.TB,相邻单位之间都是 1024 倍,1024 为 2 的 10 次方,即: 1Byte = 8bit ...

  5. 痴情研究java内存中的对象

    前记: 几天前,在浏览网页时偶然的发现一道以前就看过很多遍的面试题,题目是:"请说出'equals'和'=='的区别",当时我觉得我还是挺懂的,在心里答了一点(比如我们都知道的:' ...

  6. 《Android游戏开发详解》一2.18 使用Java API中的对象

    本节书摘来异步社区<Android游戏开发详解>一书中的第2章,第2.18节,译者: 李强 责编: 陈冀康,更多章节内容可以访问云栖社区"异步社区"公众号查看. 2.1 ...

  7. 要求或禁止在堆中产生对象

    这是摘自<More Effective C++ 2007> 条款27:要求或禁止在堆中产生对象 要求在堆中建立对象 让我们先从必须在堆中建立对象开始说起.为了执行这种限制,你必须找到一种方 ...

  8. 【C++】栈中实例化对象与堆中实例化对象

    一. 实例化对象写法的区别 栈中实例化对象: 堆中实例化对象: 最大区别就在于,从栈中申请内存,在使用完后无需释放,但是从堆中申请内存,使用完以后必须释放! 注意:从堆中申请内存,是有可能失败的,所以 ...

  9. java 堆中的新生代_Java堆内存_Young Gener_Old Generation_新生代和老年代

    使用JDK8 Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象. 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young ).老年代 ( Old ).新 ...

最新文章

  1. 朴素贝叶斯法(二)——基本方法
  2. 正则表达式测试工具、网页版
  3. gdb调试caffe工程
  4. vs2010 qt中文乱码 最终版
  5. IDEA 的文件夹的类型说明
  6. wingIDE右侧文件列表移动到左侧
  7. TypeScript 里 never 类型的用法举例
  8. 对抗告警疲劳的8种方法
  9. uctools.php,discuz 论坛UCenter无法登录,闪退的终极8种解决办法
  10. 贝叶斯之垃圾邮件分类
  11. SQLServer2005中的CTE递归查询得到一棵树
  12. 华为机试HJ28:素数伴侣
  13. 【图像增强】基于matlab模糊集图像增强【含Matlab源码 394期】
  14. 如何下载谷歌浏览器官方最新离线安装包
  15. 《Python金融大数据风控建模实战》 第15章 神经网络模型
  16. unity打开excel表格_unity创建编辑读取EXCEL文件表格数据游戏插件工具Uni-Excel 1.0
  17. 台式计算机识别不了u盘启动,台式机进入不到U盘启动怎么办
  18. vue中的this.$el
  19. 高学历就意味着高薪资?低学历转行3D建模,游戏建模成为首选
  20. Java应用程序的运行机制(介绍)

热门文章

  1. Method Swizzle黑魔法,修改 ios 系统类库方法(转载)
  2. 142. Linked List Cycle II
  3. Linux中exit与_exit的区别
  4. Python服务器开发三:Socket
  5. oracle安装出现getproces,oracle安装问题
  6. php string slice,substring()与str.slice()区别
  7. ac ap方案 华为_华为无线_AC+AP小型无线网络配置实验_v1
  8. linux运维选择题,初学Linux练习题
  9. C语言开发笔记(六)实参和形参
  10. “精彩极了”和“糟糕透了”