5.3.5 堆

Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对 象引用,虚拟机就必须能够快速地定位对象实例的数据。另外,它也必须能通过该对象引用访 问相应的类数据(存储于方法区的类型信息)。因此在对象中通常会有一个指向方法区的指针。

—种可能的堆空间设计就是,把堆分为两部分:一个句柄池,一个对象池,如图5-5所示。 而一个对象引用就是一个指向句柄池的本地指针。句柄池的每个条目有两部分:一个指向对象 实例变量的指针, 一个指向方法区类型数据的指针。这种设计的好处是有利于堆碎片的整理, 当移动对象池中的对象时,句柄部分只需要更改一下指针指向对象的新地址就可以了——就是,句柄池中的那个指针。缺点是每次访问对象的实例变量都要经过两次指针传递。这种对象表示的方法在图5-5中绘出。第9章有一个这种堆的交互演示一HeapOfFish applet。

另一种设计方式是使对象指针直接指向一组数据,而该数据包括对象实例数据以及指向方 法区中类数据的指针。这样设计的优缺点正好与前面的方法相反,它只需要一个指针就可以访 问对象的实例数据,但是移动对象就变得更加复杂。当使用这种堆的虚拟机为了减少内存碎片 而移动对象的时候,它必须在整个运行时数据区中更新指向被移动对象的引用。图5-6描绘了这种表示对象的方法。

 

象是否的确是被引用的对象或者它的超类型。当程序在执行instanccof操作时,虚拟机也进行了 同样的检查。在这两种情况下,虚拟机都需要查看被引用的对象的类数据。最后,当程序中调 用某个实例方法时.虚拟机必须进行动态绑定,换句话说,它不能按照引用的类型来决定将要 调闬的方法,而必须根据对象的实际类:为此,虚拟机必须再次通过对象的引用去访问类数据。

不管虚拟机的实现使用什么样的对象表示法,很可能每个对象都有一个方法表,因为方法 表加快了调用实例方法时的效率,从而对Java虚拟机实现的整体性能起着非常重要的正面作用。 但是java虚拟机规范并未要求必须使用方法表,所以并不是所有实现中都会使用它。比如那些具 有严格内存资源限制的实现,或许它们根本不可能有足够的额外内存资源来存储方法表。如果

一个实现使用方法表,那么仅仅使用一个指向对象的引用,就可以很快地访问到对象的方法表。

囝5-7展示了一种把方法表和对象引用联系起来的实现方式。每个对象的数据都包含一个指 向特殊数据结构的指针,这个数据结构位于方法区,它包括两部分:

•一个指向方法区对应类数据的指针。

•此对象的方法表。

方法表是个指针数组,其中的每一项都是一个指向“实例方法数据”的指针,实例方法可 以被那类的对象调用。方法表指向的实例方法数据包括以下信息:

•此方法的操作数找和局部变量区的大小。

•此方法的字节码。

•异常表。

这些信息足够虚拟机去调用一个方法了。方法表中包含有方法指针——指向类或其超类声 明的方法的数据。也就是说,方法表所指向的方法可能是此类声明的,也可能是它继承下来的。更多关于方法表的内容可以在第8章找到。


图5-5和图5-6中显示的还有另一种数据,堆上的对象数据中还有一个逻辑部分,那就是对象 锁,这是一个互斥对象。虚拟机中的每个对象都有一个对象锁,它被用于协调多个线程访问同 一个对象时的同步。在任何时刻,只能有一个线程“拥有”这个对象锁,因此只有这个线程才 能访问该对象的数据。此时其他希望访问这个对象的线程只能等待,直到拥有对象锁的线程释 放锁:当某个线程拥有一个对象锁后,可以继续对这个锁追加请求。但请求几次必须对应地
释放几次,之后才能轮到其他线程。比如一个线程请求了三次锁,在它释放三次锁之前,它一 直保持“拥有"这个锁。

很多对象在其整个生命周期内都没有被任何线程加锁。在线程实际请求某个对象的锁之前, 实现对象锁所需要的数据是不必要的。这样正如图5-5和图5-6所示,很多实现不在对象自身内部 保存一个指向锁数据的指针。而只存当第一次需要加锁的时候才分配对应的锁数据,但这时虚 拟机需要用某种间接方法来联系对象数据和对应的锁数据,例如把锁数据放在一个以对象地址 为索引的搜索树中。

除了实现锁所需要的数据外,每个java对象逻辑上还与实现等待集合(wait set)的数据相 关联。锁是用来实现多个线程对共享数据的互斥访问的,而等待集合是用来让多个线程为完成 一个共同目标而协调工作的。

等待集合由等待方法和通知方法联合使用。每个类都从Object那里继承了三个等待方法(三 个名为wait ()的重载方法)和两个通知方法(notifyO及notifyAIl())。当某个线程在一个对象

上调用等待方法时,虚拟机就阻塞这个线程,并把它放在了这个对象的等待集合中。直到另一 个线程在同一个对象上调用通知方法,虚拟机才会在之后的某个时刻唤醒一个或多个在等待集 合中被阻塞的线程。正像锁数据一样,在实际调用对象的等待方法或通知方法之前,实现对象 的等待集合的数据并不是必需的。因此,许多虚拟机实现都把等待集合数据与实际对象数据分 开,只有在需要时才为此对象创建同步数据(通常是在第一次调用等待方法或通知方法时)。关 于锁和等待集合的更多内容,请参见第20章。

最后一种数据类型-可以作为堆中某个对象映像的一部分,是与垃圾收集器有关的数据。

垃圾收集器必须(以某种方式)踉踪程序引用的每个对象,这个任务不可避免地要附加一些数 据给这些对象,数据的类型要视垃圾收集使用的算法而定。例如,假如垃圾收集器使用“标记 并清除”算法’这就需要能够标记对象能否被引用。此外,对于不再被引用的对象,还需要指 明它的终结方法(finalizer)是否已经运行过了。像线程锁一样,这些数据也可以放在对象数据
外。有一些垃圾收集技术只在垃圾收集器运行时需要额外数据。例如“标记并清除”算法就使 用一个独立的位图来标记对象的引用情况。几种不同的垃圾收集器技术,以及它们每一种所需 要的数据,请参见第9章。

除了标记对象的引用情况外,垃圾收集器还要区分对象是否调用了终结方法。对于在其类中声明了终结方法的对象,在回收它之前,垃圾收集器必须调用它的终结方法。java语言规范指出,垃圾收集器对每个对象只能调用一次终结方法,但是允许终结方法复活(resurrect)这个对 象,即允许该对象被再次引用。这样当这个对象再次被回收时,就不用再调用终结方法了。需 要终结方法的对象不多,而需要复活的更少,所以对一个对象回收两次的情况很少见。这种用 来标志终结方法的数据虽然逻辑上是对象的一部分,但通常实现上不随对象保存在堆中。大部 分情况下,垃圾收集器会在一个单独的空间保存这个信息。第9章有关于终结过程的详细内容。

数组的内部表示在Java中,数组是真正的对象。和其他对象.样,数组总是存储在堆中。 同样,和普通对象一样,实现的设计者将决定数组在堆中的表示形式。
和其他所有对象一样,数组也拥有一个与它们的类相关联的Class实例,所有具有相同维度 和类型的数组都是同一个类的实例,而不管数组的长度(多维数组每一维的长度)是多少。例如一个包含3个int整数的数组和一个包含300个int整数的数组拥有同一个类。数组的长度只与实例数据有关。

数组类的名称由两部分组成:每一维用一个方括号“[”表示,用字符或字符串表示元素类型。比如,元素类型为int整数的、一维数组的类名为“[丨”,元素类型为byte的三维数组为“[[[B”, 元素类型为Object的二维数组为“[[Ljava/lang/Object”。这些数组类的命名约定将在第6章详细 讨论。

多维数组被表示为数组的数组。比如,int类型的二维数组,将表示为一个一维数组,其中 的每个元素是一个一维int数组的引用,如图5-8所示。

在堆中的每个数组对象还必须保存的数据是数组的长度、数组数据,以及某些指向数组的类数据的引用。虚拟机必须能够通过一个数组对象的引用得到此数组的长度,通过索引访问其元素(其间要检查数组边界是否越界),调用所有数组的直接超类Object声明的方法等等。

原文地址:https://www.cnblogs.com/mongotea/p/11979460.html