Java 中 JVM 的原理(上)

2017/02/18 Java

随着Java技术的不断发展,JVM成为了开发人员无法逃避的一个知识点,今天就借助这篇文章,聊一聊Java虚拟机!

Java虚拟机的生命周期

Java虚拟机开始于main()方法, main()方法是程序的起点,它被执行的线程初始化为程序的初始线程,程序中其他线程都由它来启动。Java中的线程分为守护线程 (daemon)和普通线程(non-daemon)。只要Java虚拟机中还有普通线程在执行,Java虚拟机就不会停止。如果有足够的权限,可调用exit()方法终止程序。

Java虚拟机运行时数据区

Java虚拟机执行Java程序时把内存区域分为若干个数据区域,具体如下图:

Java虚拟机运行时数据区

程序计数器

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,故在任意时刻,一个处理器(对于多核处理器来说就是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。

如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,则这个计数器则为空。

Java虚拟机栈

虚拟机栈也是线程私有,而且生命周期与线程相同,每个Java方法执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表(存放各种基本数据类型和对象引用)、操作数栈、动态链接、方法出口等信息。

Java虚拟机规范对该区域规定了两种异常情况:如果线程请求栈深度大于虚拟机允许深度,抛出StackOverflowError异常;虚拟机栈可以动态拓展,当扩展时无法申请到足够内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈的作用与虚拟机栈作用类似,只不过前者针对Native方法,后者针对Java方法。

Java堆

对大多数应用来说,Java堆(Heap)是Java虚拟机所管理的内存中最大的一块,Java堆被所有线程共享,在虚拟机启动时创建。该内存区域唯一的目的就是存放对象实例,Java对象实例以及数组都在堆上分配(随着JIT编译器发展,所有对象分配在堆上也不是那么绝对了)。Java堆是垃圾收集器管理的主要区域,因此Java堆也被称为GC堆。

根据Java虚拟机规范的规定,Java堆可处于物理上不连续的内存空间中,只要逻辑连续即可。如果堆中没有足够内存完成实例分配,而且堆也没办法再扩展时,将会抛出OutOfMemoryError异常。

方法区

方法区也是被线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范对方法区限制非常宽松,除了不需要连续内存外,还可选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域比较少出现,但并非数据进入了方法区就成为了永久代。该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。Java虚拟机规范规定当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池

运行时常量池是方法区的一部分,存放编译期生成的各种字面量和符号引用。运行时常量池具备动态性,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中。

虚拟机类加载机制

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段,如下图所示:

虚拟机类加载机制

加载、验证、准备和初始化这四个阶段的顺序是确定的,而解析阶段则不一定,它可在初始化后开始,这是为了支持 Java 的运行时绑定。对 Java 来说,绑定分为静态绑定和动态绑定:静态绑定即在程序执行前方法已经被绑定,只有 final,static,private 和构造方法是前期绑定的;动态绑定即在运行时根据具体对象的类型进行绑定。

加载

在加载阶段,虚拟机完成三件事情:通过类的全限定名获取其定义的二进制字节流;将字节流代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成代表这个类的java.lang.Class对象,作为对方法区中数据的访问入口。

加载阶段(准确说是加载阶段获取类的二进制字节流的动作)可控性最强,因为开发人员既可使用系统的类加载器加载,也可自定义类加载器完成加载。任意一个类,都需要由它的类加载器和这个类本身确定其在就 Java 虚拟机中的唯一性,即使两个类来源于同一Class文件,只要类加载器不同,那这两个类就不相等。

验证

验证的目的是确保 Class 文件中的字节流包含的信息符合虚拟机的要求,且不危害虚拟机安全。不同虚拟机对类验证的实现可能不同,但大致都会完成四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。

  • 文件格式的验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理,该验证保证字节流能解析并存储于方法区。经过该验证,字节流才会进入方法区,后面三个验证都是基于方法区的存储结构进行的。
  • 元数据验证:对类的元数据信息进行语义校验(其实就是对类中各数据类型进行校验),保证不存在不符合 Java 语法规范的元数据信息。
  • 字节码验证:进行数据流和控制流分析,对类的方法体进行校验分析,以保证类的方法运行时不会危害虚拟机安全。
  • 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,主要对类自身以外的信息(常量池中的各种符号引用)进行匹配性校验。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都在方法区分配。对于该阶段有几点需要注意:

  • 进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随对象分配在 Java 堆。
  • 设置的初始值通常是数据类型默认的零值,而不是被在显式赋予的值。

假设一个类变量的定义如下,那么变量在准备阶段过后的初始值为0,而不是3。

public static int value = 3;

如果类字段的字段属性表存在ConstantValue属性,即同时被final和static修饰,那么准备阶段变量就会被初始化为指定值。假设上面的类变量被定义如下,则变量在准备阶段过后的初始值为3。

public static final int value = 3;

解析

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

  • 类或接口的解析:判断要转化的直接引用是数组类型的引用,还是普通对象类型的引用,从而进行不同解析。
  • 字段解析:先在本类查找名称和字段描述符都与目标匹配的字段,如果有,查找结束;如果没有,按继承关系从上往下递归搜索该类实现的接口和它们的父接口,还没有,则按继承关系从上往下递归搜索其父类,直至查找结束。
  • 类方法解析:与字段解析类似,只是多了判断该方法所处的是类还是接口的步骤,且对类方法的匹配搜索,是先搜索父类,再搜索接口。
  • 接口方法解析:与类方法解析类似,只是接口不会有父类,因此只递归向上搜索父接口。

初始化

在准备阶段,类变量已被赋过一次初始值,而在初始化阶段,则是根据程序指定去初始化类变量和其他资源。

Search

    Table of Contents