Java虚拟机(五)

当一个类实例或者数组在java程序中被创建时,这些新对象的内存从一个单一的堆中分配。一个java虚拟机实例中只有一个堆,并被所有的线程共享,所以我们在程序中进行多线程访问对象时需要考虑同步的问题。

java虚拟机有为对象在堆中分配内存的指令,但却没有释放内存的指令。java虚拟机自身会负责决定什么时候应该将程序中不在引用的对象所占用的内存释放掉。通常,java虚拟机是实现中使用垃圾收集器来管理堆内存。


垃圾回收

垃圾收集器的主要功能是自动回收利用程序中不再被引用的对象的内存。


对象的表示

每个对象中必须以某种方式表示的主要数据是实例变量。给定一个对象引用,虚拟机必须能够快速地定位到对象的实例数据。此外,通过这给定的对象引用还必须可以以某种方式访问到对象的类数据(存储在方法区里)。因此,为对象分配的内存中通常包含一些指向方法区的指针。

一种可能的堆设计把堆分成了两部分:一个句柄池和一个对象池。一个对象引用是一个指向句柄池条目的指针,一个句柄池条目包含两部分:一个指向对象池中实例数据的指针和一个指向方法区中类数据的指针。这种设计方案的好处是它使得java虚拟机可以更简单地处理堆中的碎片。当虚拟机移动对象池中的对象时,它只需要用这个对象的新地址更新一个指针(句柄池中相应的指针)。这种方法的缺点是对一个对象实例的访问需要进行两次指针取值。这种对象表示方法的图示如下:


另一种设计是使对象引用成为一个包含对象实例和对象的类数据指针的本地指针。这种方法在访问对象实例数据时只需要进行一次地址取值,但是移动对象就变得复杂了,当虚拟机要移动一个对象来处理堆碎片时,它必须为每个对象的引用进行更新。这种方法的图示如下:

虚拟机有以下几个原因需要从对象引用中得到这个对象的类数据。当一个运行的程序尝试将一个对象引用进行类型转换时,虚拟机必须检查这个转换的类型是否是引用对象的类型或者是它的父类,当执行instanceof操作时也要进行同样的检查。当程序调用一个实例方法时,虚拟机需要执行动态绑定(通过对象的类型而不是引用的类型来选择调用的方法),这样,虚拟机需要根据给定的对象引用访问类型的数据。

因为方法表加快了实例方法调用的速度,所以在提高虚拟机整体性能上起着重要的作用。方法表并不是java虚拟机规范中的一部分,所以不是所有的虚拟机实现中都有方法表。对于有方法表的虚拟机实现,给定一个对象引用应该可以快速地访问到对象的方法表。

一种方法表与对象引用关联的方式如下图所示:

在图示的结构中,与每个对象实例数据放在一起的指针指向了一个特殊结构,这个特殊的结构由两部分组成:
  • 一个指向类型数据的指针
  • 对象的方法表

方法表是每个实例方法数据的指针数组,方法表指向的方法数据包括:
  • 操作数栈的大小和方法栈中的本地变量部分
  • 方法的字节码
  • 异常表
堆中的对象数据还包含另一种类型的数据,对象锁(lock)。java虚拟机中的每个对象都与一个锁相关联,程序通过这个锁来协调对象的多线程访问。在某个时刻只能有一个线程可以获得对象锁。当一个特定的线程拥有了一个特定的锁,只有那个线程可以访问那个对象的实例变量,其他尝试访问这个对象的变量的线程必须等待,直到拥有锁的线程将对象锁释放掉。如果一个线程请求一个已经被其他线程拥有的锁,这个请求的线程必须等待直到拥有锁的线程将锁释放。一旦一个线程拥有了对象锁,它可以对同一个锁进行多次请求,但是在释放的时候需要进行相应次数的释放。例如,如果一个线程对一个锁请求了三次,那么这个会持续拥有这个锁知道它进行了三次释放。

很多对象在整个生命周期都没有被一个线程锁定,只有在线程请求对象锁时,对象锁的数据才需要存储。因此,很多实现可能没有在对象里包含一个指向锁的指针。这种实现方式必须在第一次请求锁的时候创建锁的数据。这种设计方案,虚拟机必须将对象与锁以某种间接的方式关联起来,例如把锁数据放到一个基于对象地址的搜索树中。

与实现了lock的数据一起,每个java对象逻辑上是与一个实现了wait set的数据相关联的。lock帮助线程在共享数据上可以独立工作免受线程相互间的干扰,而wait set则帮助线程之间相互合作,一起完成一个共同的任务。

wait set是结合wait和notify方法一起使用的,每个类从Object中继承三个wait方法和两个nofity方法(notify()和notifyAll())。当一个线程调用一个对象的wait方法时,java虚拟机会挂起那个线程并把它添加到那个对象的wait set中。当一个线程调用对象的notify方法时,虚拟机会在未来的某个时间从那个对象的wait set中唤醒一个或多个线程。和实现了对象锁的数据一样,实现对象wait set的数据只有在调用了wait或者notify方法的对象中才需要。因此,很多java虚拟机实现都会将wait set数据与实际的对象数据分离。这种实现可以在wait或者notify方法第一次被程序调用时才对wait set的数据进行内存分配。

垃圾收集器所需要的数据也可能被包含在对象中。垃圾收集器必须以某种方式跟踪在程序中被引用的对象,这项任务总是要求数据被堆上的对象保存起来。需要的数据类型由使用垃圾回收技术决定。例如,如果一种实现使用了标记清除算法,它必须能够标记一个对象为被引用或者没有被引用。对于每个没有被引用的对象,它可能也需要表明该对象的finalizer是否已经被执行。跟线程锁一样,这个数据可以与对象镜像分开存储。一些垃圾回收技术只有在垃圾收集器运行时才需要这些额外的数据。例如,标记清扫算法可能会使用一个独立的点阵图来表示被引用和没被引用的对象。

除了用来标记被引用和没有被引用的对象的数据,垃圾收集器还需要数据用来跟踪哪个对象已经执行了一个finalizer。如果一个对象的类声明了一个finalizer,那么在它的内存被回收之前,垃圾收集器需要执行这个finalizer。java语言规范中规定垃圾收集器只会执行一次对象的finalizer,允许finalizer复活一个对象(使得对象再次被引用)。当这个对象第二次没有被引用时,垃圾收集器就不会再次执行finalizer。因为大多数对象可能会没有finalizer,并且它们之中只有很少的一部分会复活对象,对同一个对象进行两次垃圾回收的情况是很少出现的,因此,用来记录对象已经被finalized的数据虽然逻辑上数据与对象关联的一部分,但是可能不会与堆中的对象表示放在一起,垃圾收集器会把这些信息保存在一个独立的空间。


数组的表示

在java中,数组是完备的对象,跟对象一样,数组总是存储在堆中,并且有一个与它的类关联的Class实例。所有维度和类型相同的数组拥有同样的class。数组的长度(或者多维数组每个维度的长度)在确立数组的类型上不发挥任何作用,数组的长度属于它的实例数据的一部分。数组class的名字由左边的方括号(每个维度一个)和一个代表数组类型的字母或者字符串组成。例如,一维int数组的class名为“[I]”。三维byte数组的的名字为“[[[B”。二维Object数组的class名为“[[Ljava.lang.Object”。

多维数组被表示为数组的数组。例如,一个二维int数组会被表示为一维int数组引用的一维数组。如下图所示:

每个数组需要在堆中保存的数据有数组的长度,数组的数据和一个对数组class数据的引用。给定一个组数的引用,虚拟机必须能够确定数组的长度,通过索引来get和set它的数据(检查确定数组没有越界)。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值