学习目标:虚拟机的内存管理,包括对虚拟机的初步认识、类的装载、堆和栈。
Java虚拟机
Java虚拟机(JVM)也是一个可运行的程序,它或者全部用软件方式来实现,或者采用硬件或软件结合的方式来实现。当启动一个Java程序时,就会运行一个Java虚拟机,每个Java程序都会对应一个Java虚拟机,运行中的Java虚拟机,也可以称为Java虚拟机的一个实例。Java程序运行结束关闭后,运行这个程序的Java虚拟机也会关闭。例如,如果在电脑运行了三个Java程序,就会有三个Java虚拟机在运行。

每个Java程序的主类都会有一个main()方法,虚拟机启动Java程序时,它会调用主类的main()方法作为Java程序的入口,这个main()方法的访问权限必须是public,并且是无返回值的静态方法,方法的参数是一个String类型的数组。
下面是HelloWorld程序代码:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
HelloWorld程序只有一个HelloWorld类,HelloWorld类内部必须有一个main()方法,当HelloWorld程序运行时,虚拟机会调用HelloWorld类的main()方法作为HelloWorld程序的入口。
在Windows命令行窗口运行HelloWorld程序的命令如下:
java HelloWorld
其中“java”就是虚拟机程序名称,Windows会启动一个虚拟机实例,“java”后面的“HelloWorld”是包含main()方法已编译的class文件名称,虚拟机启动后,会加载HelloWorld.class文件到内存,并调用HelloWorld.class文件内的main()方法,HelloWorld程序进入运行状态,main()方法内部的语句会得到执行,当main()方法内部的所有语句都执行完成后,main方法返回,HelloWorld程序结束,运行这个HelloWorld程序的虚拟机实例也会退出。
虚拟机的体系结构
虚拟机由类加载器、运行数据区、执行引擎、本地方法接口、本地方法库组成。虚拟机体系结构如下图所示:

当虚拟机运行一个程序时,它会使用类加载器加载Java程序的类文件,并且在计算机内申请一块内存区域,作为运行数据区。用来存储程序的类文件、创建的静态对象和实例对象、方法、局部变量、中间结果、方法的返回值等内容。为了管理和有效地利用已申请的内存区域,虚拟机把内存划分为堆、栈、方法区、程序计数器、本地方法栈。
运行数据区的方法区存储了包含程序使用import语句导入的JDK类库在内的类信息、各种常量、静态变量、代码编译后的字节码等内容;本地方法栈用来存储调用非java代码编写的方法(Native Method)使用到的数据,这些方法包括用其它语言编写的第三方库和操作系统自身提供的API;堆用来存储程序中动态创建的类实例对象;栈用来存储程序调用方法过程中,使用到的参数、局部变量、中间结果、返回值等内容;程序计数器用来存储当前程序执行的指令(指令可以理解为类中的可执行代码)。
执行引擎负责执行方法区中的字节码,在执行字节码的过程中会在堆中创建实例对象,在栈中存取当前方法调用的参数、局部变量、中间结果、返回值等,也会通过本地方法接口调用第三方库方法和操作系统自身提供的API。
类装载过程
类的装载就是把class文件加载到虚拟机的运行数据区,虚拟机的类加载器负责把class文件加载到运行数据区。
类加载器在装载class文件的过程中,会把class文件转换为方法区的内部数据结构,并创建一个表示该类型的java.lang.Class类的实例,Class类的实例存储到数据运行区的堆。
创建Class类的实例后,类加载器会验证加载的类是否符合Java语义规范,并且它不会对虚拟机造成破坏或者导致虚拟机崩溃。类加载器也会检查该类的所有父类是否都已经被加载。
类装载成功后,虚拟机会为类变量分配内存,设置默认初始值,同时会为类方法构建方法表,该方法表存储指向每一个方法(包括从父类继承的方法)的内存地址,当虚拟机需要调用类方法时,直接从方法表中查找方法的内存地址。类变量、方法和方法表都存储在虚拟机运行数据区的方法区。
类装载的最后一个步骤就是初始化,也就是为类变量赋予正确的初始值,正确的初始值是程序员在编写代码时赋予变量的值,不是前面设置的默认初始值。在Java语言中,变量正确的初始值是通过变量初始化语句或者静态初始化语句给出的。所有的类变量初始化语句和静态初始化语句都被Java编译器收集在一起,并放置到一个名称为“<clinit>”方法中,这个方法在已编译好的class文件中,虚拟机在初始化类的过程中,会调用这个方法。
初始化一个类包含下面两个步骤:
(1) 如果该类存在父类,且父类没有被初始化时,先初始化父类;
(2) 如果存在一个类初始化方法,就执行这个方法。
当初始化一个父类时,也需要包含上面的两个步骤,因此顶层的Object类最先被初始化,然后是类的继承链上所有的类。
当一个类的变量使用static和final修饰符时,虚拟机会把该类变量放置到常量池中,常量池是从堆划分出来的一块内存区域,主要用于存放类的字面常量(如数值)、被static和final修饰的类变量、符号引用
堆和栈
关于运行数据区的堆
堆是在运行数据区划分出来的一块内存区域,用于存储在程序运行过程中创建的对象实例和数组,在虚拟机运行的所有线程创建的对象实例和数组都共享一个堆。
堆中的存储空间是有限的,当堆中存储的对象实例超过堆的存储空间时,堆就无法再存储新的实例对象,在这种情况下就会造成堆的溢出,java程序也会抛出内存溢出异常。因此当堆中的实例对象不再需要时,应及时回收空间,回收的空间再分配给新的实例对象。
那么,在什么情况下要对堆中的实例对象进行回收呢?开发者可以使用new运算符在堆中创建实例对象,但不能释放已创建的实例对象。其实开发者不需要考虑如何回收实例对象占用的存储空间,虚拟机的垃圾收集器(垃圾收集器在后面的课程会讲到)会自动回收不再被运行的程序引用的对象所占用的存储空间。
关于运行数据区的栈
栈是一个数据结构,栈结构是一种特殊的线性表,限定仅在表的一端进行元素的插入和删除。当表中没有元素时,称为空栈。若给定栈:
S = (a1,a2,……,an)
则称a1是栈底元素,an是栈顶元素,表中元素按a1,a2,……,an的次序进栈,出栈的顺序是an,……,a2,a1。也就是说,栈结构的元素访问原则是后进先出,也称为后进先出的线性表的,如下图所示。

栈也是在运行数据区划分出来的一块内存区域,栈是和线程相关的,虚拟机会为每个线程分配一个栈,栈以帧为单位保存线程的运行状态,一个栈帧保存了Java方法的参数、局部变量、中间运算结果、返回值等数据。栈帧由局部变量区、操作数栈和栈数据区构成。当线程调用一个Java方法时,虚拟机会从方法所在的类信息中得知此方法的局部变量区和操作数栈的大小,并给栈帧分配内存,将栈帧压入栈中。
每当线程调用一个方法时,虚拟机都会在该线程的栈中压入一个栈帧,这个栈帧为当前栈帧。Java方法有两种返回方式:一种是方法运行完成后,以return方式返回;一种是在方法运行过程中,发生错误抛出异常,非正常返回。不管以哪种方式返回,虚拟机都会将当前栈帧弹出并释放掉,这样上一个方法的栈帧就成为当前栈帧了。
堆和栈的关系
堆是存储实例对象和数组的内存区域,栈是存储线程内类方法运行状态的内存区域。当线程调用类方法时,与方法有关的类变量和方法内部的局部变量都会存储到栈帧,当这些类变量和局部变量是对象引用变量时,这些变量会指向堆中的实例对象或数组,方法执行完成后,与方法相关的栈帧被弹出栈,栈帧占用的存储空间被释放,但堆中的实例对象或数组并没有释放,它们由垃圾收集器在随后的时间进行释放。
