Java堆中对对象的管理

来源:互联网 发布:java二叉树遍历相互求 编辑:程序博客网 时间:2024/04/28 03:42
在大概了解了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中就是使用的这种访问方式。
0 0
原创粉丝点击