本来计划要写 Android 内存优化的,觉得有必要在此之前介绍一下 Java 虚拟机的相关知识,Java 虚拟机也并不是三言两语能够介绍完的,因此开了 Java 虚拟机系列,这一篇文章我们来学习 Java 虚拟机的结构原理与运行时数据区域。
Oracle 官方定义的 Java 技术体系主要包括以下几个部分:
可以把 Java 程序设计语言、Java 虚拟机和 Java API 类库这三部分统称为 JDK(Java Development Kit),它是 Java 程序开发的最小环境。另外,Java API 中的 Java SE API 子集和 Java 虚拟机这两部分统称为 JRE(Java Runtime Environment),它是 Java 程序运行的标准环境。 从上面可以看出 Java 虚拟机及其重要,它是整个 Java 平台的基石,是 Java 语言编译代码的运行平台。你可以把 Java 虚拟机看做一个抽象的计算机,它有各种指令集和各种运行时数据区域。
很多同学可能认为 Java 虚拟机,就是一个虚拟机而已,它还有家族?或者认为 Java 虚拟机指的就是 Oracle 的 HotSpot 虚拟机。这里来简单介绍 Java 虚拟机家族,自从 1996 年 Sun 公司发布的 JDK1.0 中包含的 Sun Classic VM 到今天,出现和消亡了很多种虚拟机,我们这里只简单介绍目前存活的相对主流 Java 虚拟机。
HotSpot VM Oracle JDK 和 OpenJDK 中自带的虚拟机,是最主流的和使用范围最广的 Java 虚拟机。介绍 Java 虚拟机的技术文章,如果不做特殊说明,大部分都是介绍 HotSpot VM 的。HotSpot VM 并非是 Sun 公司开发的,而是由 Longview Technologies 这家小公司设计的,它在 1997 年被 Sun 公司收购,Sun 公司又在 2009 年被 Oracle 收购。 J9 VM J9 VM 是 IBM 开发的 VM,目前是其主力发展的 Java 虚拟机。J9 VM 的市场定位和 HotSpot VM 接近,它是一款设计上从服务端到桌面应用再到嵌入式都考虑到的多用途虚拟机,目前 J9 VM 的性能水平大致跟 HotSpot VM 是一个档次的。 Zing VM 以 Oracle 的 HotSpot VM 为基础,改进了许多会影响延迟的细节。最大的三个卖点是:
当我们执行一个 Java 程序时,它的执行流程是怎样的呢?如下图所示。
从上图可以看到 Java 虚拟机与 java 语言没有什么必然联系,它只与特定的二进制文件:Class 文件有关。
这里所讲的体系结构,是指的 Java 虚拟机的抽象行为,而不是具体的比如 HotSpot VM 的实现。按照 Java 虚拟机规范,抽象的 Java 虚拟机如下图所示。
Java 文件被编译后生成了 Class 文件,这种二进制格式文件不依赖于特定的硬件和操作系统。每一个 Class 文件中都对应着唯一一个类或者接口的的定义信息,但是类或者接口并不一定定义在文件中,比如类和接口可以通过类加载器来直接生成。
ClassFile 的文件结构如下所示。
- ClassFile {
- u4 magic;//魔数,固定值为0xCAFEBABE,用来判断当前文件是能被Java虚拟机处理的Class文件u2 minor_version;//副版本号u2 major_version;//主版本号u2 constant_pool_count;//常量池计数器cp_info constant_pool[constant_pool_count-1];//常量池u2 access_flags;//类和接口层次的访问标志u2 this_class;//类索引u2 super_class;//父类索引u2 interfaces_count;//接口计数器u2 interfaces[interfaces_count];//接口表u2 fields_count;//字段计数器field_info fields[fields_count];//字段表u2 methods_count;//方法计数器method_info methods[methods_count];//方法表u2 attributes_count;//属性计数器attribute_info attributes[attributes_count];//属性表}
类加载器子系统通过多种类加载器来查找和加载 Class 文件到 Java 虚拟机中。Java 虚拟机有两种类加载器:系统加载器和用户自定义加载器。其中系统加载器包括以下三种:
用户自定义加载器,则是通过继承 java.lang.ClassLoader 类的方式来实现自己的类加载器。 类加载器子系统除了要加载 Class 文件类到 Java 虚拟机中,还必须负责验证被导入的 Class 类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。这些动作必须严格按以下顺序进行:
1. 装载:查找并加载 Class 文件。 2. 链接:验证、准备、以及解析。
3. 初始化:将类变量初始化为正确初始值。
Java 虚拟机与 Java 语言的数据类型相似,可以分为两类:基本类型和引用类型。Java 虚拟机希望编译器在编译期间尽可能的完成类型检查,使得虚拟机在运行期间无需进行类型检查操作。
很多人将 Java 的内存分为堆内存(heap)和栈内存(Stack),这种分发不够准确,Java 的内存区域划分实际上远比这复杂。 Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为不同的数据区域,根据《Java 虚拟机规范(Java SE7 版)》的规定,这些数据区域分别为程序计数器、Java 虚拟机栈、本地方法栈、Java 堆和方法区,下面我们来一一的对它们进行介绍。
为了保证程序能够连续地执行下去,处理器必须具有某些手段来确定下一条指令的地址,而程序计数器正是起到这种作用。 程序计数器(Program Counter Register)也叫做 PC 寄存器,是一块较小的内存空间。在虚拟机概念模型中,字节码解释器工作时就是通过改变程序计数器来选取下一条需要执行的字节码指令,Java 虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的,在一个确定的时刻只有一个处理器执行一条线程中的指令,为了在线程切换后能恢复到正确的执行位置,每个线程都会有一个独立的程序计数器,因此,程序计数器是线程私有的。如果线程执行的方法不是 Native 方法,则程序计数器保存正在执行的字节码指令地址,如果是 Native 方法则程序计数器的值则为空(Undefined)。程序计数器是 Java 虚拟机规范中唯一没有规定任何 OutOfMemoryError 情况的数据区域。
每一条 Java 虚拟机线程都有一个线程私有的 Java 虚拟机栈(Java Virtual Machine Stacks)。它的生命周期与线程相同,与线程是同时创建的。Java 虚拟机栈存储线程中 Java 方法调用的状态,包括局部变量、参数、返回值以及运算的中间结果等。一个 Java 虚拟机栈包含了多个栈帧,一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息。当线程调用一个 Java 方法时,虚拟机压入一个新的栈帧到该线程的 Java 栈中,当该方法执行完成,这个栈帧就从 Java 栈中弹出。我们平常所说的栈内存(Stack)指的就是 Java 虚拟机栈。 Java 虚拟机规范中定义了两种异常情况:
Java 虚拟机实现可能要用到 C Stacks 来支持 Native 语言,这个 C Stacks 就是本地方法栈(Native Method Stack)。它与 Java 虚拟机栈类似,只不过本地方法栈是用来支持 Native 方法服务。如果 Java 虚拟机不支持 Native 方法,并且也不依赖于 C Stacks,可以无需支持本地方法栈。在 Java 虚拟机规范中对本地方法栈的语言和数据结构等没有强制规定,因此具体的 Java 虚拟机可以自由实现它,比如 HotSpot VM 将本地方法栈和 Java 虚拟机栈合二为一。 与 Java 虚拟机栈类似,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常
Java 堆(Java Heap)是被所有线程共享的运行时内存区域。Java 堆用来存放对象实例,几乎所有的对象实例都在这里分配内存。Java 堆存储的对象被垃圾收集器管理,这些受管理的对象无需也无法显示的销毁。从内存回收的角度,Java 堆可以粗略的分为新生代和老年代。从内存分配的角度 Java 堆中可能划分出多个线程私有的分配缓冲区。不管如何划分,Java 堆存储的内容是不变的,进行划分是为了能更快的回收或者分配内存。 Java 堆的容量可以时固定的,也可以动态的扩展。Java 堆的所使用的内存在物理上不需要连续,逻辑上连续即可。 Java 虚拟机规范中定义了一种异常情况:
方法区(Method Area)是被所有线程共享的运行时内存区域。用来存储已经被 Java 虚拟机加载的类的结构信息,包括: 运行时常量池、字段和方法信息、静态变量等数据。方法区是 Java 堆的逻辑组成部分,它一样在物理上不需要连续,并且可以选择在方法区中不实现垃圾收集。方法区并不等同于永久代,只是因为 HotSpot VM 使用永久代来实现方法区,对于其他的 Java 虚拟机,比如 J9 和 JRockit 等,并不存在永久代概念。 Java 虚拟机规范中定义了一种异常情况:
运行时常量池 运行时常量池(Runtime Constant Pool)是方法区的一部分。在 2.1 Class 文件格式这一小节中我们得知,Class 文件不仅包含了类的版本、接口、字段和方法等信息,还包含了常量池,它用来存放编译时期生成的字面量和符号引用,这些内容会在类加载后存放在方法区的运行时常量池中。运行时常量池可以理解为是类或接口的常量池的运行时表现形式。 Java 虚拟机规范中定义了一种异常情况: 当创建类或接口时,如果构造运行时常量池所需的内存超过了方法区所能提供的最大值,Java 虚拟机会抛出 OutOfMemoryError 异常。
参考资料 《深入理解 Java 虚拟机 第二版》 《Java 虚拟机规范(Java SE7 版)》 理解 Java 虚拟机体系结构 目前主流的 Java 虚拟机有哪些?- 知乎 jvm 运行时数据区域解析 Java 虚拟机原理图解 深入探讨 Java 类加载器
来源: http://blog.csdn.net/itachi85/article/details/70301145