InsideJVM(4)-Heap(堆)

类别:Java 点击:0 评论:0 推荐:

 

 


一个java应用在运行中所创建的所有类实例或数组都放在了同
一个堆中,并由应用所有的线程共享。因为一个java应用
唯一对应了一个jvm实例,所以每个应用都独占了一个堆,它
不可能对另一个应用的堆进行破坏。然而,一个多线程应用必须
考虑同步问题。

jvm有在堆中分配对象的指令,却没有释放对象的指令。正如
你无法用java代码去释放一个对象一样,字节码也没有对应的
功能。应用本身不用去考虑何时和用什么方法去回收不用对象所
占用的内存。通常,jvm把这个任务交给垃圾收集器。

垃圾收集
一个垃圾收集器的主要工作是回收不再被引用的对象所占用
的内存。它也可能去移动仍然使用的对象以减少内存碎片。

jvm规范没有指定垃圾收集使用什么技术,这些都由jvm的实现者
去定夺。因为对象的引用可能存在很多地方,如java堆栈,堆,
方法区,native方法栈。所以垃圾收集技术的使用在很大程度上
影响了运行数据区的设计。

象方法区一样,堆不必是一块连续的内存区,也可以根据应用的需要
动态调整大小。可以把方法区放在堆的顶部,换句话说就是类型信息和实际
对象都在同一个堆上。负责清理对象的垃圾收集器可能也要负责类的回收
。堆的初始化大小,最大最小尺寸可以由用户或程序指定。

对象表现( Object Representation)
(译者:C++中称为对象模型)
jvm规范没有规定对象在堆中该如何表现。对象的表现会影响堆和
垃圾收集的整个设计,它由jvm的实现者决定。

对象的主要数据是由对应类和其父类声明的实例变量组成
(instance variables 译者:对应Class variables,
Class variables存储在方法区中,这在上篇译文中有讲)
jvm应该既能够从一个对象引用快速的找到实例变量,也能够快速
的找到存储在方法区中的类数据。所以在对象中常常会有一个指向
方法区的指针。

一个可能的实现是把堆分成两部分:一个句柄池和一个对象池。如图5-5
一个对象引用是一个指向句柄池的native指针。句柄池的每个条目有
两部分:一个指向对象实例变量的指针,一个指向方法区类型数据的指针。
这种设计的好处是利于堆碎片的整理,当移动对象以减少碎片时不用更新
每个对象引用而只更改句柄就可以了。缺点是每次访问对象都要经过两次
指针传递。

图5-5

另一种设计是使对象指针直接指向对象实例变量,而在对象实例内包含一个
指向方法区类型数据的指针。这样的设计的优缺点正好与前面的方法相反。
如图5-6.

图5-6

jvm有若干理由使它能够从对象引用中得到对应类的数据。
1。 当应用试图转型(cast)时,jvm需要保证要转的类型是此类型本身
    或是这个类型的父类型。
2。 当应用进行 instanceof 操作时
3。 当应用激活一个实例方法时,jvm必须进行动态绑定,而它所依赖的信息
    不是这个引用的类型,而是这个对象对应的类的信息。   

不管对象以什么形式表现,好像都有一个能够方便访问的方法表。由于方法表
能加速实例方法的调用,所以对jvm的性能有重要的影响。jvm规范并没有规定
必须要使用方发表,例如在内存稀少的环境下,可能不能负担方法表的内存支出。
然而如果使用了方法表,它就应该能够快速的从一个对象引用中获得。

图5-7

图5-7显示了一种连结方法表和对象引用的实现。每个对象的数据包含一个
指向特殊数据结构的指针,这个数据结构位于方法区,它包括两部分:
一 一个指向方法区对应类数据的指针
二 此对象的方法表
   方法表的每一项都是一个指向方法数据的指针,方法数据包括:
   一 此方法的操作数堆栈和局部变量区的大小
   二 方法的字节码
   三 异常表
这些信息足够jvm去激活一个方法了。方法表的函数指针包括类或其父类声明的
函数。也就是说,方法表所指向的函数可能是此类声明的,也可能是它继承下来
的。

如果你熟悉c++的内部工作原理,你会发现这和c++的vtbl非常相似。在c++
中,对象由实例数据和一组指向虚拟函数的指针组成,jvm也可以采用这种方法。
jvm可以在堆中为每个对象都附加一个方法表,这样较之图5-7会占用更多的内存,
但可提高一些效率。这个方案适用在内存足够充裕的系统。
(译者:总觉得作者对c++有些误解,c++的对象模型在函数表上的设计和图5-7
     是类似的,在有虚拟函数的情况下(不考虑多继承),每个对象也只多出
     一个指向vtable的指针,而vtable也是与类关联的。)

除了图5-5和5-6显示的实例数据外,对象数据还有一个逻辑部分,那就是对象
锁(object's lock)。在jvm中每个对象都有一个锁,以用于多线程访问时的
同步。在某个时刻只有一个线程拥有这个对象锁,而且只有这个线程才可以对
对象的数据进行访问。其他要访问这个对象的线程只有等待,直到拥有对象锁的
线程释放所有权。当一个线程拥有对象锁后,可以继续对锁追加请求。但请求
几次,必须对应释放几次。

许多对象在其生命期内可能不需要加锁,这样也不需要附加数据,正如图5-5 5-6
所示,对象数据内没有一个指向锁数据(lock data)的指针。而只有当需要加锁的
时候才分配对应锁数据,但这时需要其他的方法来联系对象数据和对应的锁数据,
例如把锁数据放在一个以对象地址为索引的树中。

除了实现锁需要的数据,每个java对象逻辑上还有为实现同步而添加的数据。
锁是用来实现多个线程对共享数据的互斥访问,而同步则是实现多个线程为
完成一个共同目标而协调工作。

同步由等待方法和通知方法共同实现。每个类都从Object那里继承了
三个等待方法(三个名为wait()过载函数)和两个通知方法(notify()
和notifyAll())。当一个线程在一个对象上调用wait方法,jvm就
阻塞了这个线程并把它放在了这个对象的等待集(wait set)中。当
有一个线程在这个对象调用了通知方法,jvm就会在将来的某个时间
唤醒一个或多个在等待集中阻塞的线程。正像锁数据一样,并不是
每个对象都需要同步数据。许多jvm实现都把同步数据与对象数据分开,
只有在需要时才为此对象创建同步数据,一般是在第一次调用等待方法
或通知方法时。

最后,一个对象还可能要包含与垃圾收集有关的数据。垃圾收集必须
要跟踪每个对象,这个任务不可避免的要附加一些数据,数据的类型
要视垃圾收集的算法而定。例如,假如垃圾收集使用标志清除算法,
必须要一个数据来标志此对象是否被引用。像线程锁一样,这些数据
也可以放在对象外。一些垃圾收集技术只在运行时需要额外数据。例如
标志清除算法使用一个位图来标志对象的引用情况。

除了标志对象的引用情况外,垃圾收集还要区分一个对象是否调用
了finalizer。在收集一个对象之前,垃圾收集器必须调用声明了
finalizer的类的对象。java语言规范指出垃圾收集器对某个对象
只能调用finalizer一次,在finalizer中允许这个对象复生
(resurrect),即使之再次被引用。这样当这个对象再次被收集时,
就不再调用finalizer了。需要finalizer的对象不多,而复生的
对象更少,所以对一个对象回收两次的情况很少见。这样用来标志
finalizer的数据虽然逻辑上是对象的一部分,但通常与对象分开
保存。

数组表现
再java中,数组是一个成熟的对象。像其他对象一样,数组也存储在
堆上,jvm实现的设计者也有权决定数组的表现。

数组也有一个相关的类实例(Class instance),所有具有相同维度
和类型的数组同为一个类,而不管数组的长度(多维数组每一维的长度)。
例如一个有三个ints的数组和一个有六个ints的数组都是同一个类。
数组的长度只与实例数据有关。

数组类的名称由两部分组成,一个是用'['表示的维和一个字符表示的类型。
例如,类型为ints的一维数组的类名为“[I”。类型为bytes的三维数组为
“[[[B”。类型为Object的二维数组为“[[Ljava.lang.Object”。

多维数组被表示为数组的数组。例如,类型为ints的二维数组,将表示为
一个一维数组,数组元素是一个一维ints数组的引用。如图5-8

图5-8

每个数组必须保存的数据是数组的长度,jvm必须能够从一个数组的引用得到
此数组的长度,通过下标访问数组元素,检查数组下标是否越界,激活Object
声明的方法。

 

本文地址:http://com.8s8s.com/it/it18503.htm