
关注本公众号,一天一个知识点,持续精进!


00、前世今生
Sun官方所定义的Java技术体系包括了以下几个重要组成部分:
Java程序设计语言
各种硬件平台上的Java虚拟机
Class文件格式
Java API类库
来自商业机构和开源社区第三方Java类库
Java技术体系所包含的具体组件、工具,详见Fig.1。我们可以把Java程序设计语言、Java虚拟机、Java API类库这三部分统称为JDK,JDK是用于支持Java程序开发的最小环境;可以把Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE,JRE是支持Java程序运行的标准环境。

Fig.1 Java技术体系
下面,我们给出上述所涉及到的几个重要概念的比较正式与严谨的定义:
JDK(Java Development Kit)
Java 开发者工具集,是用来编译和执行 Java 程序必备的 Java 开发环境,现在我们一般说 JDK 就是指 Oracle 的 Java SE。因为 Sun JDK、Open JDK(Sun将JDK开源后形成的版本)和JRockit (BEA公司开发)都被 Orcale 收购,实现了真正的统一。
JRE(Java Runtime Environment)
Java 运行环境,也就是Java平台。所有的Java程序都要在JRE下才能运行。JDK目录下有很多工具也是Java程序,也需要JRE才能运行,为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
JVM(Java Virtual Machine)
Java虚拟机,也是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
以上是根据各个组成部分的功能来进行划分的,如果按照技术所应用的业务领域来划分,Java技术体系则可以分为四个平台:
Java Card
支持一些Java小程序(Applets)运行在小内存设备(如智能卡)上的平台。
Java ME(Micro Edition)
支持Java程序运行在移动终端(手机、PDA)上的平台,对Java API有所精简,并加入了针对移动终端的支持,这个版本以前称为J2ME。
Java SE(Standard Edition)
支持面向桌面级应用(如Windows下的应用程序)的Java平台,提供了完整的Java核心API,这个版本以前称为J2SE。
Java EE(Enterprise Edition)
支持使用多层架构的企业应用(如EPR、CRM应用)的Java平台,除了提供Java SE API外,还对其做了大量的扩充并提供了相关的部署支持,这个版本以前称为J2EE。
知识链接:Android之Dalvik VM
Dalvik VM是Android平台的核心组成部分之一,它的名字来源于冰岛一个名为Dalvik的小渔村。Dalvik VM并不是一个Java虚拟机,它没有遵循Java虚拟机规范,不能直接执行Java的Class文件,使用的是寄存器架构而不是JVM中常见的栈架构。但是它与Java又存在着千丝万缕的联系,它执行的dex文件(Dalvik Executable)文件可以通过Class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API等。
目前Dalvik VM随着Android一起处于迅猛发展阶段,在Android2.2中已提供即时编译实现,在执行性能上有了很大的提高。
在Java技术体系图,Java虚拟机(以后简称为JVM)是作为最底层的存在,平时我们在开发、运行程序的时候,JVM对开发者、应用者是透明的。“勿在浮沙筑高台”,如果我们要开发高效率、高质量、高安全的Java程序,那么深入理解JVM的工作原理与运行机制是非常有必要的。
程序 = 数据结构 + 算法。从本质上讲,JVM也是一个程序,要深入理解JVM,也就是要全面理解其数据结构和算法。今天我们先从数据结构谈起,具体而言就是JVM里面的内存模型。
01、JVM运行时的内存模型
JVM在执行Java程序的过程中会把TA所管理的内存区域按照其不同的用途划分为若干个数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域是随着JVM进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。这些内存区域包括了5个部分,如Fig.2所示。
程序计数器
Java虚拟机栈
本地方法栈
Java堆
方法区

Fig.2 从JVM运行时内存模型看HotSpot JVM架构
1 程序计数器
程序计数器是一块较小的内存空间,可把它看作当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址。
注意:如果当前线程正在执行的是一个本地方法,那么此时程序计数器为空。
A. 程序计数器的作用
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
B. 程序计数器的特点
占用存储空间很小。
线程私有。每个线程都有属于自己的一个程序计数器。
是唯一一个不会出现OutOfMemoryError的内存区域。
生命周期随着线程的创建而创建,随着线程的结束而死亡。
2 Java虚拟机栈
Java虚拟机栈是描述Java方法运行过程的内存模型。Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,这块区域用于存储该方法在运行过程中所需要的一些信息,这些信息包括:
局部变量表。存放基本数据类型变量、引用类型的变量、返回类型的变量。
操作数栈。
动态链接。
方法出口信息。
当一个方法即将被运行时,Java虚拟机栈首先会在Java虚拟机栈中为该方法创建一块“栈帧”,栈帧中包含局部变量表、操作数栈、动态链接、方法出口信息等。当方法在运行过程中需要创建局部变量时,就将局部变量的值存入栈帧的局部变量表中。当这个方法执行完毕后,这个方法所对应的栈帧将会出栈,并释放内存空间。可通过-Xss选项设置Java虚拟机栈的大小。
注意:“堆”与“栈”的认知?
人们常说,Java的内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。
这句话不完全正确!“堆”基本可以理解为存放对象;“栈”也可以基本理解为Java虚拟机栈。但是,按照上面的说法,“栈”只包括了Java虚拟机栈中的局部变量表部分。
真正的Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。为此,上面的简单的理解太多片面,并没有全面说明栈所包含的全部内容。
A. Java虚拟机栈的特点
局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。而且,局部变量表的大小在编译时期就确定下来了,在创建的时候只需分配事先规定好的大小即可。此外,在方法运行的过程中局部变量表的大小是不会发生改变的。
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常;若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
注:StackOverFlowError和OutOfMemoryError的异同?
StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。
3 本地方法栈
本地方法栈和Java虚拟机栈实现的功能类似(注:HotSpot虚拟机中,直接就把本地方法栈和虚拟机栈合二为一),只不过本地方法区是本地方法运行的内存模型。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间。也会抛出StackOverFlowError和OutOfMemoryError异常。
4 Java堆
堆是用来存放对象的内存空间。 所有的对象实例以及数组都要在堆上进行分配。
A. Java堆的特点
线程共享。整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个的。
在JVM启动时创建。
垃圾收集器管理的主要区域。可以进一步细分为:新生代、老年代。新生代又可被分为:Eden空间、From Survior空间、To Survior空间等。不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,从而更高效。
堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的(通过虚拟机的启动参数-Xmx和-Xms控制),因此当线程请求分配内存,但堆中没有内存完成实例分配,且堆也无法再扩展时,将会抛出 OutOfMemoryError。-Xms为JVM可申请的最小堆内存,-Xmx为JVM可申请最大堆内存,为了避免在运行时频繁调整堆的大小,通常将-Xms和-Xmx的值设置成一样。
5 方法区
方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然JVM规范把方法区描述为一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是为了和Java堆区分开来。
注:方法区 == 永久代吗?
对于熟悉HotSpot的老铁而言,很多人愿意把方法区称为“永久代”。但是本质上两者并不等价,仅仅是因为HotSpot JVM开发团队选择把GC分代收集扩展至方法区,或者说是用永久代来实现方法区。对于其他类型的JVM来说不存在永久代。
A. 方法区的特点
线程共享。方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。
永久代方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为老年代。
内存回收效率低。方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。 对方法区的内存回收的主要目标是:对常量池的回收 和 对类型的卸载。
Java虚拟机规范对方法区的要求比较宽松。 和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。
B. 运行时常量池
运行时常量池是方法区的一部分。方法区中存放三种数据:类信息、常量、静态变量、即时编译器编译后的代码。其中常量存储在运行时常量池中。
我们一般在一个类中通过public static final来声明一个常量。这个类被编译后便生成Class文件,这个类的所有信息都存储在这个class文件中。当这个类被JVM加载后,class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如:String类的intern()方法就能在运行期间向常量池中添加字符串常量。
当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。
在HotSpot中,方法区默认最小值为16MB,默认最大值为64MB;可通过-XX:PermSize及-XX:MaxPermSize来指定最小值和最大值。
6 直接内存
直接内存是除Java虚拟机之外的内存,但也有可能被Java使用。
从JDK1.4开始,在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率。
直接内存的大小不受Java虚拟机控制,但是当本机物理内存不足时就会抛出OutOfMemoryError错误。
02、小结
综上,我们对JVM在Java技术体系中所处的位置、作用,及其运行时的内存模型进行了详细说明。
在下篇文章中,我们会从垃圾回收、线程运行、内存功能设置等多个维度去理解JVM的内存模型,其目的是将众多知识点串联起来。敬请期待。





