JVM 是一个从事 Java 开发的软件工程师的修炼之路上必然要翻阅的一座山。当你了解了 Java 的基本语言特性,当你熟悉了 Java SDK 中的常用 API,当你写过一些或大或小的程序后,你就会有去了解 JVM 的需求出现。如果你现在没有这种感觉,那么可能此时去了解 JVM 并不是一个好的时机,因为你不会带着问题去探索。
从本篇开始的系列博文,记录本人的 JVM 深入学习总结,其中结合了本人自己的一些经验,也参考了一些书籍和网络资源,然后根据自己的理解写出这些博文。如有版权问题,请伊妹儿我 :)
谨以此系列博文分享给我的朋友们。
屏蔽不同的硬件平台或操作系统上的环境差异,通过一个向上层提供统一编程接口来实现Java程序可移植性的软件层,我们称之为 Java 虚拟机(Java Virtual Machine,简称 JVM)。
虽然 Java 的发展史可以追溯到 1991年4月由著名的 James Gosling 领导的 Green Project 计划,但是 JDK 1.0 版本的正式发布是在 1996年的1月23日,该版本提供的 JVM 是一个纯解释执行的 Sun Classic VM,不过是以外部加载的方式来使用的。而该版本的 JDK 所包含的主要技术除了 JVM 之外,就是 Applet 和 AWT。当然,此前在 Java 还叫做 Oak 的时候就已经有了一个完整的编程语言的外形,而 1995年5月23日,Oak 正式更名为 Java,并由 Sun 公司发布了 Java 1.0 版本。
关于 Java 语言的背景,这里就不多说了,主要还是介绍 JVM 的发展历程。到 1998年发展出了 JDK 1.2,在该版本中 JVM 内置了 JIT (Just In Time) 编译器,而 JDK 1.2 中也曾有过 Sun Classic VM、Hot Spot VM 和 Sun Exact VM 三种虚拟机。其中 Hot Spot VM 和 Extract VM 都内置 JIT 编译器。1997年,Sun 收购了开发 Hot Spot VM 的名为 Longview Technologies 的公司。也从此该虚拟机改叫 Sun Hot Spot VM,当然那么一个前缀对于 Developers 来说是没所谓的。从 JDK 1.3 开始,Sun Hot Spot VM 成为 Sun 公司发布的 JDK 的默认 JVM。
目前活跃的商用 JVM 有 Sun Hot Spot、BEA JRockit 和 IBM J9。不过要说的是,JRockit 的主人 BEA 被 Oracle 收购了,而 Hot Spot 的主人被 Sun 公司在 2010 年也被 Oracle 收购了。因此 Hot Spot 和 JRockit 都隶属于 Oracle 公司。Oracle 曾称将会将这个两个 JVM 的优势相融合,产生一款新的 JVM,届时 Hot Spot 和 JRockit 也将进入历史博物馆了。JVM 的鼻祖 Sun Classic VM 早已被淘汰使用了,而 曾在 JDK 1.2 中灵光乍现过的 Sun Extract VM 也已经退出了历史舞台。另一个由 Apache 基金会主导的 Harmony 项目也有很大的影响,且间接由其催生的 Dalvik 虚拟机,为 Google Android 的火爆发展做出了巨大的贡献。在应用于手机、平板电脑、IVI、PDA 等设备上的嵌入式 JVM 领域,除了 Dalvik,还有 KVM、CDC Hot Spot、CLDC Hot Spot 等 JVM 也较有影响力。
从本文开始的系列博文《JVM 原理与实战》中所有实验性程序的环境,都是 Mac OS X 10.7.3,JDK 1.6.0 Update 29,Oracle Hot Spot 20.4-b02。
大多数 JVM 将内存区域划分为 Method Area(Non-Heap), Heap, Program Counter Register, Java Method Stack,Native Method Stack 和 Direct Memomry(注意 Directory Memory 并不属于 JVM 管理的内存区域)。前三者一般译为:方法区、堆、程序计数器。但不同的资料和书籍上对于后三者的中文译名不尽相同,这里将它们分别译作:Java 方法栈、原生方法栈和直接内存区。对于不同的 JVM,内存区域划分可能会有所差异,比如 Hot Spot 就将 Java 方法栈和原生方法栈合二为一,我们可以同城为方法栈(Method Stack)。
首先我们熟悉一下一个一般性的 Java 程序的工作过程。一个 Java 源程序文件,会被编译为字节码文件(以 class 为扩展名),然后告知 JVM 程序的运行入口,再被 JVM 通过字节码解释器加载运行。那么程序开始运行后,都是如何涉及到各内存区域的呢?
概括地说来,JVM 每遇到一个线程,就为其分配一个程序计数器、Java 方法栈和原生方法栈。当线程终止时,两者所占用的内存空间也会被释放掉。栈中存储的是栈帧,可以说每个栈帧对应一个“运行现场”。在每个“运行现场”中,如果出现了一个局部对象,则它的实例数据被保存在堆中,而类数据被保存在方法区。
我们用上面这一小段文字就描述完了每个内存区域的基本功能。但是这还比较粗糙,下面分别介绍它们的存储对象、生存期与空间管理策略。
这个最简单,就先捡它说吧。程序计数器,是线程私有(与线程共享相对)的,也就是说有 N 个线程,JVM 就会分配 N 个程序计数器。如果当前线程在执行一个 Java 方法,则程序计数器记录着该线程所执行的字节码文件中的指令地址。如果线程执行的是一个 Native 方法,则计数器值为 Undefined。
程序计数器的生存期多长呢?显然程序计数器是伴随线程生而生,伴随线程死而死的。而它所占用的内存空间也很小。
Java 方法栈也是线程私有的,每个 Java 方法栈都是由一个个栈帧组成的,每个栈帧是一个方法运行期的基础数据结构,它存储着局部变量表、操作数栈、动态链接、方法出口等信息。当线程调用调用了一个 Java 方法时,一个栈帧就被压入(push)到相应的 Java 方法栈。当线程从一个 Java 方法返回时,相应的 Java 方法栈就弹出(pop)一个栈帧。
其中要详细介绍的是局部变量表,它保存者各种基本数据类型和对象引用(Object reference)。基本数据类型包括 boolean、byte、char、short、int、long、float、double。对象引用,本质就是一个地址(也可以说是一个“指针”),该地址是堆中的一个地址,通过这个地址可以找到相应的 Object(注意是“找到”,原因会在下面解释)。而这个地址找到相应 Object 的方式有两种。一种是该地址存储着 Pointer to Object Instance Data 和 Pointer to Object Class Data,另一种是该地址存储着 Object Instance Data,其中又包含有 Pointer to Object Class Data。如下两图所示。
图1·句柄方式的 图2·直接方式第一种方式,Java 方法栈中有 Handler Pool 和 Instance Pool。无论哪种方式,Object Class Data 都是存储在方法区的,Object Instance Data 都是存储在堆中的。
原生方法栈与 Java 方法栈相类似,这里不再赘述。
堆是在启动虚拟机的时候划分出来的区域,其大小由参数或默认参数指定。当虚拟机终止运行时,会释放堆内存。一个 JVM 只有一个堆,它自然是线程共享的。堆中存储的是所有的 Object Instant Data 以及数组(不过随着栈上分配技术、标量替换技术等优化手段的发展,对象也不一定都存储在堆上了),这些 Instance 由垃圾管理器(Garbage Collector)管理,具体的算法会在后面提到。
堆可以是由不连续的物理内存空间组成的,并且既可以固定大小,也可以设置为可扩展的(Scalable)。
通过(2)中 Java 方法栈的介绍,你已经知道了 Object Class Data 是存储在方法区的。除此之外,常量、静态变量、JIT 编译后的代码也都在方法区。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 Non-Heap。方法区也可以是内存不连续的区域组成的,并且可设置为固定大小,也可以设置为可扩展的,这点与堆一样。
方法区内部有一个非常重要的区域,叫做运行时常量池(Runtime Constant Pool,简称 RCP)。在字节码文件中有常量池(Constant Pool Table),用于存储编译器产生的字面量和符号引用。每个字节码文件中的常量池在类被加载后,都会存储到方法区中。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如 String 类中的 intern() 方法产生的常量。
直接内存区并不是 JVM 管理的内存区域的一部分,而是其之外的。该区域也会在 Java 开发中使用到,并且存在导致内存溢出的隐患。如果你对 NIO 有所了解,可能会知道 NIO 是可以使用 Native Methods 来使用直接内存区的。
[1] 关于 Exact VM 的资料较少,我是在《深入理解 Java 虚拟机》一书中首次了解到的。
-
转载请注明来自“柳大的CSDN博客”:blog.csdn.net/Poechant
-