上篇文章描述了Runtime Date Areas(运行时数据区)这一大块中的Method Area(方法区),此篇文章接着上一篇的内容往下讲。
Heap 堆
java程序在运行时创建的所有类实例或数组都放在同一个堆中。而一个Java虚拟机实例只存在一个堆内存空间,因此所有的线程均共享这个堆。又由于一个java程序独占一个Java虚拟机实例,因而每个java程序都有他自己的堆内存----他们之间不会相互影响。但是同一个Java程序的多线程却共享着同一个堆空间,在这种情况下,就得考虑多线程访问对象(堆数据)的同步问题了。
java虚拟机规范中并没有规定具体的实现需要准备多少内存,也没有说他必须怎么去管理他的堆空间,它仅仅告诉设计者
java程序需要在堆内存中为对象分配空间,并且程序本身并不去释放他。因此堆空间的管理(包括垃圾收集)问题得由设计者自己自行去考虑处理方式。对象的引用在很多地方都存在,如java栈、堆、方法区、本地方法栈,所以垃圾收集技术的设计和使用在很大程度上会影响到运行时数据区的设计,对于垃圾收集技术的深入描述,在后续的文章中会单独拿出来讨论。
和方法区一样,堆空间也不必是连续的内存区。在程序运行时,他可以动态的扩展或收缩。事实上,一个实现的方法区可以在堆顶实现。换句话说就是当虚拟机需要为某个新装载的类分配内存是,类型信息和实际对象可以都在同一个堆上。因此,负责回收无用对象的垃圾收集器可能也要负责无用类的释放。另外,某些实现允许程序员或用户指定堆的初始化内存大小以及最大、最小值等。
对象的内部表示
Java虚拟机规范并没有规定Java对象在堆中是如何表示的。对象的内部表示也影响着整个堆以及垃圾收集器的设计,他由虚拟机的实现者决定。
java对象中包含的基本数据类型由他所属的类及其所有超类声明的实例变量组成。只要有一个对象引用虚拟机就必须能够快速定位对象实例的数据。另外,他也必须能够通过该对象引用访问相应的类数据(存储于方法区的类型信息)。因此在对象中通常会有一个指向方法区的指针。
一种可能的堆空间设计就是,把堆分为两个部分:一个句柄池,一个对象池。如下图所示,
一个对象引用就是一个指向句柄池的指针。句柄池的每个条目有两个部分:一个指向对象实例变量的指针,一个指向方法区中类型数据的指针。这种设计的好处是有利于碎片的整理,当移动对象池中的对象时,句柄部分只需要更改一下指针指向对象的新地址即可----就是句柄池中的那个指针。缺点是每次访问对象的实例变量时都要经过两次指针传递。
另一种设计方式是使对象指针直接指向一组数据,而该数据包括对象实例数据以及指向方法区中类数据的指针。
这样设计的优缺点正好和前面的相反,他只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂。当使用这种堆的虚拟机为了减少内存碎片而移动对象的时候,他必须在整个运行时数据区中更新指向被移动对象的引用。
有如下几个理由要求虚拟机必须能够通过对象引用得到类数据:当程序在运行时需要转换某个对象引用为另一种数据类型时,虚拟机必须要检查这种转换是否被允许,被转换的对象是否的确是被引用的对象或者他的超类,当程序在执行instanceof操作时,虚拟机也进行了同样的检查。在这两种情况下,虚拟机都需要查看被引用对象的类数据。最后,当程序中调用某个实例方法时,虚拟机必须进行动态绑定,换句话说,他不能按照引用类型来决定将要调用的方法,而必须根据对象的实际类,为此,虚拟机必须再次通过对象的引用去访问类数据。
不管虚拟机的实现使用什么样的对象表示法,很可能每个对象都有一个方法表,因为对象表加快了调用实例方法时的效率,从而对java虚拟机实现的整体性能起着非常重要的作用。但是Java虚拟机规范中并未要求必须是使用方法表,所以并不是所有的实现中都会使用它。比如那些具有严格内存资源限制的实现,或者他们根本不可能有足够的额外内存资源来存储方法表。如果一个实现中使用方法表,那么仅仅使用一个指向对象的引用,就可以很快访问到对象的方法表。
上图展示了一种把方法表和对象引用联系起来的实现方式。每个对象的数据都包含一个指向特定数据结构的指针,这个数据结构位于方法区,他包括两部分:
一个指向方法区对于类数据的指针
此对象的方法表
此方法的操作数栈和局部变量区的大小
此方法的字节码
异常表
图5-7的堆内存设计会占用更多的内存,但是可以轻微提高一些效率,一般来讲,该方案只适用于内存足够充裕的系统。
图5-5和图5-6中显示的还有另外一种数据,堆上的对象数据中还有一个逻辑部分,那就是对象锁,这是一个互斥对象。虚拟机中的每个对象都有一个对象锁,他被用于协调多个线程访问同一个对象时的同步。在任何时刻,只能有一个线程“拥有”这个对象锁,因此只有这个线程才能访问该对象的数据。此时其他希望访问这个对象的线程只能等待,直到拥有对象锁得线程释放锁,当某个线程拥有一个对象锁后,可以继续对这个所追加请求。但请求几次,必须对应地释放几次,之后才能轮到其他线程。比如一个线程请求了三次锁,在他释放三次锁之前,他一直保持“拥有”这个锁。
很多对象在其整个生命周期内都没有被任何线程枷锁。在线程实际请求某个对象的锁之前,实现对象锁所需要的数据是不必要的。这样正如图5-5和图5-6所示,很多实现不在对象自身内部保持一个指向锁数据的指针。而只有当第一次需要枷锁的时候才分配对应的锁数据,但这是虚拟机需要某种间接方法来联系对象数据和对应的锁数据。例如把锁数据放在一个以对象地址为索引的搜索树中。
除了实现锁所必须的数据外,每个Java对象逻辑上还与实现等待集合(wait set)的数据相关联。锁是用来实现多线程对共享数据的互斥访问的,而等待集合是用来让多个线程为完成一个共同目标而协调工作的。
等待集合由等待方法和通知方法联系使用。每个类都从Object哪里集成了三个等待方法(三个名为wait()的重载方法)和两个通知方法(notify()以及notifyAll())。当某个线程在一个对象上调用等待方法时,虚拟机就阻塞这个线程,并把他放在了这个对象的等待集合中。知道另外一个线程在同一个对象上调用通知方法,虚拟机才会在之后的某个时刻唤醒一个或多个在等待集合中被阻塞的线程。正像锁数据一样,在实际调用对象的等待方法或通知方法之前,实现对象的等待集合的数据并不是必须的。二次,许多虚拟机实现都吧等待集合数据与实际对象数据分开,只有在需要时菜为此对象创建同步数据(通常是在第一次调用等待方法或通知方法时)。关于锁和等待集合的内容,在特定章节再具体阐述。
最后一种数据类型--------可以作为堆中某个对象映像的一部分,是与垃圾收集器有关的数据。垃圾收集器必须(以某种方式)跟踪程序引用的每个对象,这个任务不可避免地要附加一些数据给这些对象,数据的类型要视垃圾收集使用的算法而定。此外,对于不再被引用的对象,还需要指明他的终结方法(finalizer)是否已经运行过了。像线程锁一样,这些数据也可以放在对象数据外。有一些垃圾收集技术只在垃圾收集器运行时需要额外的数据。例如“标记并清除”算法就使用一个独立的位图来标记对象的引用情况。
除了标记对象的引用外,垃圾收集器还要区分对象是否调用了终结方法。对于在其类中声明了终结方法的对象,在回收他之前,垃圾收集器必须调用它的终结方法。java语言对反之处,垃圾收集器对每个对象只能调用一次终结方法,但是允许终结方法复活(resurrect)这个对象,即允许改对象被再次引用。这样当这个对象再次被回收时,就不用再调用终结方法了。需要终结方法的对象不多,而需要复活的更少,所以对一个对象回收两次的情况很少见。这种用来标识终结方法的逻辑上时对象的一部分,但通常实现上不随对象保存在堆中。大部分情况下,垃圾收集器会在一个单独的空间保存这个信息。
数组的内部表示
在Java中,数组是真正的对象。和其他对象一样,数组总是存储在堆中。同样,和普通对象一样,实现的设计者将决定数组在堆中的表示形式。
和其他所有对象一样,数组也拥有一个与他们的类相关联的Class实例,所有具有相同维度和类型的数组都是同一个类的实例,而不管数组的长度(多维数组每一维的长度)是多少。例如一个包含3个int整数的数组和一个包含300个int整数的数组拥有同一个类。数组的长度只与实例数据有关。
数组类的名称由两部分组成:每一维用一个方括号“[”表示,用字符或字符串表示元素类型。比如,元素类型为int整数的、以为数组的类名为“[]”,元素类型为byte的三维数组为“[[[B”,元素类型为Object的二位数组为“[[Ljava/lang/Object”。
多为数组被表示为数组的数组。比如,int类型的二维数组,将表示为一个一维数组,其中的每个元素是一个一维int数组的引用,入下图所示:
在堆中的每个数组对象还必须保存的数据是数组的长度、数组数据,以及某些指向数组的类数据的引用。虚拟机必须能够通过一个数组对象的引用得到此数组的长度,通过索引访问其元素(其间要检查数组边界是否越界)、调用所有数组的直接超类Object声明的方法等等。
未完待续
- 大小: 29.5 KB
- 大小: 25.1 KB
- 大小: 42.7 KB
- 大小: 38.3 KB
分享到:
相关推荐
总的来说,《自己动手写Java虚拟机(GO语言)》是一本结合理论与实践的优秀教材,适合对JVM感兴趣的开发者,尤其是那些希望深入理解Java平台内部运作的Go语言爱好者。通过阅读和实践,读者将能够更全面地掌握JVM的...
《自己动手写Java虚拟机》是Java核心技术系列中的一本书,旨在帮助读者深入理解Java虚拟机(JVM)的工作原理,以及如何从零开始构建一个简单的JVM。这本书通过实践的方式,让读者能够亲手实现虚拟机的关键功能,从而...
总之,《自己动手写Java虚拟机》这本书提供了一条深入学习JVM的实践之路,通过对JVM的构造和运作过程的亲身体验,读者能够更深入地理解Java平台的底层机制,这对于成为一名优秀的Java开发者或系统优化专家具有极大的...
《自己动手写Java虚拟机》是一本面向技术爱好者和Java开发者深入理解JVM原理的书籍。这个项目旨在挑战自我,通过实践来理解Java虚拟机的工作机制,从而更好地优化代码、提高程序性能。对于想要深入Java平台的人来说...
《自己动手写虚拟机》是一本深度探讨Java虚拟机(JVM)原理和技术的书籍,主要面向对计算机底层运行机制有浓厚兴趣的开发者和学生。通过亲手构建虚拟机,读者可以深入理解Java程序是如何被解释执行的,以及JVM如何...
Java虚拟机非常复杂,要想真正理解它的工作原理,最好的方式就是自己动手编写一个! 本书是继《深入理解Java虚拟机》之后的又一经典著作,它一方面遵循《Java虚拟机规范》,一方面又独辟蹊径,不仅能让Java虚拟机的...
《自己动手写Java虚拟机及class文件解析分析工具(java8运行)》是一份深入探讨Java虚拟机(JVM)工作原理以及如何解析与分析Java类文件(.class)的资源。通过使用Go语言实现一个简化的JVM,这份资料旨在帮助读者...
2. **内存管理**:JVM内存分为堆、栈、方法区、程序计数器和本地方法栈五大部分。堆是对象实例的主要存储区域,栈则用于存储方法调用的局部变量,方法区存储类的信息,程序计数器记录下一条要执行的指令,本地方法栈...
通过亲自动手构建一个简化的虚拟机,你可以学习到许多关于Java执行环境的关键知识点。以下是一些核心概念和相关知识的详细说明: 1. **Java虚拟机**:JVM是Java平台的核心组件,它为Java应用程序提供了一个运行环境...
在这个名为"JV-jvm_practice.zip"的压缩包中,你将找到一系列关于JVM相关的代码示例,帮助你深入理解JVM的工作原理。以下是对这些关键知识点的详细解释: 1. **类加载机制**: Java的类加载机制分为加载、验证、...
Java虚拟机非常复杂,要想真正理解它的工作原理,最好的方式就是自己动手编写一个!, 本书是继《深入理解Java虚拟机》之后的又一经典著作,它一方面遵循《Java虚拟机规范》,一方面又独辟蹊径,不仅能让Java虚拟机的...
Java虚拟机非常复杂,要想真正理解它的工作原理,最好的方式就是自己动手编写一个! 本书是继《深入理解Java虚拟机》之后的又一经典著作,它一方面遵循《Java虚拟机规范》,一方面又独辟蹊径,不仅能让Java虚拟机的...
7. **自己动手写Java虚拟机**:`自己动手写Java虚拟机 (Java核心技术系列.pdf`提供了一种实践性的学习方法,通过构建自己的JVM,从底层理解Java的运行机制。 8. **Effective Java**:`effective-java-2.pdf`是Java...
通过这些示例,你可以亲自动手运行代码,加深对理论知识的理解,体验实际编程过程中的问题和解决方案。每个示例都是精心设计的,旨在帮助你更好地掌握Java核心技术,并能将其应用到实际项目中。 总的来说,这个...
2. **Java进阶** - 多线程:掌握线程同步机制,如synchronized、volatile、Lock接口及其子类。 - 并发编程:理解并发容器,如ConcurrentHashMap、CopyOnWriteArrayList等。 - 文件I/O:熟悉文件读写,流的分类及...