Java虚拟机生命周期和体系结构

来自Wikioe
跳到导航 跳到搜索


Java虚拟机的生命周期

一个运行时的 Java 虚拟机负责运行一个 Java 程序。当启动一个 Java 程序时,一个虚拟机实例也就诞生了。当程序关闭退出,这个虚拟机实例也就随之消亡。如果在同一台计算机上同时运行三个 Java 程序,将得到三个 Java 虚拟机实例。每个 Java 程序都运行于它自己的 Java 虚拟机实例中。

起点

Java 虚拟机实例通过调用某个初始类的 main() 方法来运行一个 Java 程序。而这个 main() 方法必须是 public static 的,返回值为 void,并且接受一个 String[] 数组作为参数。

任何拥有这样一个 main() 方法的类都可以作为 Java 程序运行的起点。告诉 Java 虚拟机要运行的 Java 程序中初始类的名字,整个程序将从它的 main() 方法开始运行。
  • Java程序初始类中的main()方法,将作为该程序初始线程的起点,任何其他的线程都是由这个初始线程启动的。

终点

Java虚拟机内部有两种线程:守护线程非守护线程。

守护线程通常是由虚拟机自己使用的,比如执行垃圾收集任务的线程。但是,Java程序也可以把它创建的任何线程标记为守护线程。
而 Java 程序中的初始线程-就是开始于 main() 的那个是非守护线程。
  • 只要还有任何非守护线程在运行,那么这个 Java 程序也在继续运行(虚拟机仍然存活)。
  • 当程序中的所有的非守护线程都终止时,虚拟机实例自动退出。
    • 假如安全管理器允许,程序本身也能够通过调用 Runtime 类或者 System 类的 exit() 方法退出。

Java虚拟机的体系结构

在《Java虚拟机规范》中,一个虚拟机实例的行为时分别按照子系统、内存区、数据类型以及指令这几个术语来描述的。这些组成部分一起展示了抽象的虚拟机的内部抽象体系结构。

Java虚拟机运行时数据区.jpg
当Java虚拟机运行一个程序时,它需要内存来存储多个东西、例如,字节码、从已装载的 class 文件中得到的其他信息、对象、方法参数、返回值、局部变量以及运算的中间结果等等,Java 虚拟机把这些东西组织到几个“运行时数据区”中,以便于管理。
  • 尽管这些"运行时数据区"都会以某种形式存在于每一个Java虚拟机实现中,但是规范对它们的描述却是相当抽象的。这些运行时数据区结构上的细节,大多数由具体实现的设计者决定。

线程共享:“堆”、“方法区”

某些“运行时数据区”是由程序中所有线程共享的,还有一些则只能由一个线程拥有。(每个 Java 虚拟机实例都有一个“方法区”以及一个“堆”,它们是由该虚拟机实例中所有线程共享的)

当虚拟机装载一个 class 文件时,它会从这个 class 文件包含的二进制数据中解析类型信息。然后,它把这些类型信息放到“方法区”中。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到“堆”中。
JMM:线程共享的“堆”和“方法区”.png
  • Java堆(Java Heap):唯一目的就是存放对象实例以及数组,Java世界里“几乎”所有的对象实例都在这里分配内存。
    • 又称:“GC堆”,堆是垃圾收集器管理的主要区域;
  • 方法区(Method Area):用于存储已被虚拟机加载的“类型信息”、“常量”、“静态变量”、“即时编译器编译后的代码”缓存等数据。
    • JDK 8 之前,HotSpot用永久代来实现方法区;JDK 8 之后,废弃了永久代,改用“元空间”(Metaspace)来代替。
    • 运行时常量池(Runtime Constant Pool):位于方法区,用于保存静态常量引用和符号引用。(class文件中的“常量池表”,在类加载后存放到方法区的“运行时常量池”中)

线程私有:“程序计数器”、“虚拟机栈”

当每一个线程被创建时,它都将得到它自己的“程序计数器”以及一个“虚拟机栈”:

  1. 如果线程正在执行一个Java方法(非本地方法),那么程序计数器的值将总是指示下一条将被执行的指令,而它的虚拟机栈则总是存储:该线程中Java方法调用的状态——包括它的局部变量、被调用时传进来的参数、它的返回值、以及运算的中间结果等。
  2. 而本地方法调用的状态,则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或者其他某些与特定实现相关的内存区中。
Java栈是由许多栈帧组成的,一个栈帧包含一个Java方法调用的状态。

当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中:当该方法返回时,这个栈帧被从Java栈中弹出被抛弃。   
  • Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令尽量紧凑,同时也便于Java虚拟机在那些只有很少通用寄存器的平台上实现。
    • Java虚拟机的这种基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器即时编译器的代码优化。
  • Java虚拟机为每一个线程创建的内存区(程序计数器、虚拟机栈),这些内存区域是私有的。任何线程都不能访问另一个线程的程序计数器或虚拟机栈。


示例:

JMM:线程私有的“程序计数器”和“虚拟机栈”.png
如图,展示了一个虚拟机实例的快照:
  • 它有三个线程正在执行:线程1和线程2都正在执行Java方法,而线程3则正在执行一个本地方法。
  • Java栈都是向下生长的,而栈顶都显示在图的底部。当前正在执行的方法的栈帧以浅色表示,对于一个正在运行Java方法的程序而言,它的程序计数器总是指向下一条将被执行的指令。

关于“StackOverflowError”和“OutOfMemoryError”

关于 JMM 中常见的两个异常:

1、StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度,将抛出该异常;【请求大于栈深】
2、OutOfMemoryError:线程无法申请到足够的内存,会抛出该异常;【扩展超出内存】
  • 程序计数器:是“唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域”。
  • Java虚拟机栈、本地方法栈:可能出现 StackOverflowError 和 OutOfMemoryError。
  • Java堆、方法区:可能出现 OutOfMemoryError。
  • 直接内存(并非虚拟机“运行时数据区”的一部分):可能出现“OutOfMemoryError”。