JVM生命周期和类加载机制

  • Post author:
  • Post category:其他




一、java虚拟机的生命周期:


Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。 Java虚拟机总是开始于一个



main()



方法,这个方法必须是公有、返回void、直接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名。 Main()方法是


程序的起点


,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。Java中的线程分为两种:



守护线程



(daemon)和



普通线程



(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集


GC的线程就是一个守护线程


。当然,你也可 以把自己的程序用setDeamon设置为守护线程。包含Main()方法的初始线程不是守护线程。 只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。



二、java虚拟机的体系结构:

在Java虚拟机的规范中定义了一系列的子系统、内存区域、数据类型和使用指南。这些组件构成了Java虚拟机的内部结构,他们不仅仅为Java虚拟机的实现提供了清晰的内部结构,更是严格规定了Java虚拟机实现的外部行为。 
     每一个Java虚拟机都由一个类加载器子系统class loader subsystem),负责加载程序中的类型(类和接口),并赋予唯一的名字。每一个Java虚拟机都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。
     程序的执行需要一定的内存空间,如字节码、被加载类的其他额外信息、程序中的对象、方法的参数、返回值、本地变量、处理的中间变量等等。Java虚拟机将这些信息统统保存在数据区(data areas)中。虽然每个Java虚拟机的实现中都包含数据区,但是Java虚拟机规范对数据区的规定却非常的抽象。许多结构上的细节部分都留给了 Java虚拟机实现者自己发挥。不同Java虚拟机实现上的内存结构千差万别。一部分实现可能占用很多内存,而其他以下可能只占用很少的内存;一些实现可能会使用虚拟内存,而其他的则不使用。这种比较精炼的Java虚拟机内存规约,可以使得Java虚拟机可以在广泛的平台上被实现。
     数据区中的一部分是整个程序共有,其他部分被单独的线程控制。每一个Java虚拟机都包含方法区(method area)和(heap),他们都被整个程序共享。Java虚拟机加载并解析一个类以后,将从类文件中解析出来的信息保存与方法区中。程序执行时创建的对象都保存在堆中。 
     当一个线程被创建时,会被分配只属于他自己的PC寄存器“pc register”(程序计数器)和Java堆栈(Java stack)。当线程不掉用本地方法时,PC寄存器中保存线程执行的下一条指令。Java堆栈保存了一个线程调用方法时的状态,包括本地变量、调用方法的 参数、返回值、处理的中间变量。调用本地方法时的状态保存在本地方法堆栈中(native method stacks),可能再寄存器或者其他非平台独立的内存中。
     Java堆栈由堆栈块(stack frames (or frames))组成。堆栈块包含Java方法调用的状态。当一个线程调用一个方法时,Java虚拟机会将一个新的块压到Java堆栈中,当这个方法运行结束时,Java虚拟机会将对应的块弹出并抛弃。
     Java虚拟机不使用寄存器保存计算的中间结果,而是用Java堆栈在存放中间结果。这是的Java虚拟机的指令更紧凑,也更容易在一个没有寄存器的设备上实现Java虚拟机。 
     图中的Java堆栈中向下增长的,PC寄存器中线程三为灰色,是因为它正在执行本地方法,他的下一条执行指令不保存在PC寄存器中。

java 虚拟机的结构

JVM 寄存器

所有的 CPU 均包含用于保存系统状态和处理器所需信息的寄存器组。如果虚拟机定义义较多的寄存器,便可以从中得到更多的信息而不必对栈或内存 进行访问, 这有利于提高运行速度。 然而, 如果虚拟机中的寄存器比实际 CPU的寄存器多,在实现虚拟机时就会占用处理器大量的时间来用常规存储器模拟寄存器,这反而会降低虚拟机的效率。针对这种情况,JVM 只设置了 4 个最为常用的寄存器。 它们是: pc 程序计数器, optop 操作数栈顶指针 , frame当前执行环境指针, vars 指向当前执行环境中第一个局部变量的指针, 所有寄存器均为 32 位。pc 用于记录程序的执行。optop,frame 和 vars 用于记录指向 Java 栈区的指针。

JVM 栈结构

作为基于栈结构的计算机, Java 栈是 JVM 存储信息的主要方法。 当 JVM 得到一个 java 字节码应用程序后, 便为该代码中一个类的每一个方法创建一个栈框架,以保存该方法的状态信息。Java 虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。 局部变量用于存储一个类的方法中所用到的局部变量。vars 寄存器指向该变量表中的第一个局部变量。执行环境用于保存解释器对Java 字节码进行解释过程中所需的信息。它们是:上次调用的法、局部变量指针和操作数栈的栈顶和栈底指针。执行环境是一个执行一个方法的控制中心。例如:如果解释器要执行 iadd(整数加法),首先要从 frame 寄存器中找到当前执行环境,而后便从执行环境中找到操作数栈,从栈顶弹出两个整数进行加法运算,最后将结果压入栈顶。 操作数栈用于存储运算所需操作数及运算的结果。 JVM 碎片回收堆 Java 类的实例所需的存储空间是在堆上分配的。 解释器具体承担为类实例分配空间的工作。解释器在为一个实例分配完存储空间后,便开始记录对该实例所占用的内存区域的使用。 一旦对象使用完毕, 便将其回收到堆中。 在 Java语言中, 除了 new 语句外没有其他方法为一对象申请和释放内存。 对内存进行释放和回收的工作是由 Java 运行系统承担的。 这允许 Java 运行系统的设计者自己决定碎片回收的方法。 在 SUN 公司开发的 Java 解释器和 Hot Java环境中,碎片回收用后台线程的方式来执行。这不但为运行系统提供了良好的性能,而且使程序设计人员摆脱了自己控制内存使用的风险。 JVM 存储区 JVM 有两类存储区: 常量缓冲池和方法区。 常量缓冲池用于存储类名称、方法和字段名称以及串常量。方法区则用于存储 Java 方法的字节码。对于这两种存储区域具体实现方式在 JVM 规格中没有明确规定。这使得 Java 应用程序的存储布局必须在运行过程中确定, 依赖于具体平台的实现方式。 JVM是为 Java 字节码定义的一种独立于具体平台的规格描述, 是 Java 平台独立性的基础。 目前的 JVM 还存在一些限制和不足, 有待于进一步的完善, 但无论如何,JVM 的思想是成功的。对比分析:如果把 Java 原程序想象成我们的 C++原程序,Java 原程序编译后生成的字节码就相当于 C++原程序编译后的 80x86 的机器码(二进制程序文件),JVM 虚拟机相当于 80x86 计算机系统,Java 解释器相当于 80x86CPU。在 80x86CPU 上运行的是机器码,在 Java 解释器上运行的是 Java 字节码。 Java 解释器相当于运行 Java字节码的“CPU”, 但该“CPU”不是通过硬件实现的, 而是用软件实现的。 Java解释器实际上就是特定的平台下的一个应用程序。只要实现了特定平台下的解释器程序,Java 字节码就能通过解释器程序在该平台下运行,这是 Java跨平台的根本。当前,并不是在所有的平台下都有相应 Java 解释器程序,这也是 Java 并不能在所有的平台下都能运行的原因,它只能在已实现了Java 解释器程序的平台下运行。 Java 虚拟机从启动到结束的生命周期,当 java 虚拟机启动后,在如下几种情况下,Java 虚拟机将结束生命周期: 1.执行了 System.exit()方法 2.程序正常执行结束 3.程序在执行过程中遇到了异常或错误而异常终止 4.由于操作系统出现错误而导致 Java 虚拟机进程终止



三、类加载器子系统:

Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转
换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。

JVM的类加载机制:JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、解析、初始化,最终形成可以被JVM直接使用的java类型。

Java虚拟机中的类加载器分为两种:原始类加载器(primordial class loader)和类加载器对象class loader objects)。原始类加载器是Java虚拟机实现的一部分,类加载器对象是运行中的程序的一部分。不同类加载器加载的类被不同的命名空间所分割。
     类加载器调用了许多Java虚拟机中其他的部分和java.lang包中的很多类。比如,类加载对象就是java.lang.ClassLoader子类的实例,ClassLoader类中的方法可以访问虚拟机中的类加载机制;每一个被Java虚拟机加载的类都会被表示为一个 java.lang.Class类的实例。像其他对象一样,类加载器对象和Class对象都保存在中,被加载的信息被保存在方法区中。

类的生命周期:
java 类的生命周期就是指一个 class 文件从加载到卸载的全过程     
   加载、连接、初始化(Loading, Linking and Initialization)
类加载子系统不仅仅负责定位并加载类文件,他按照以下严格的步骤作了很多其他的事情:(具体的信息参见第七章的“类的生命周期”)
          1)、加载:寻找并导入指定类型(类和接口)的二进制信息
          2)、连接:进行验证、准备和解析
               ①验证:确保导入类型的正确性
               ②准备:为类型分配内存并初始化为默认值
               ③解析:将字符引用解析为直接引用
	  3)、初始化:调用Java代码,初始化类变量为正确的初始值

4)、使用

	  5)、卸载
          
从上边我们可以看出类的静态变量赋了两回值。这是为什么呢?原因是,在连接过程中时为静态变量赋值为默认值,也就是说,只要是你定义了静态变量,不管你开始给没给它设置,我系统都为他初始化一个默认值。到了初始化过程,系统就检查是否用户定义静态变量时有没有给设置初始化值,如果有就把静态变量设置为用户自己设置的初始化值,如果没有还是让静态变量为初始化值

类的加载
类的加载指的是将类的.class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。 这里的 class 对象其实就像一面镜子一样,外面是类的源程序,里面是 class 对象,它实时的反应了类的数据结构和信息。

类的连接
类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、 变量与方法是不是有重复、 数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被 jvm 所运行。 很多人都感觉, 既然这个类都通过编译加载到内存里了,那肯定就是合法的了,为什么还要验证呢,这是因为这里的验证时为了避免有人恶意编写 class 文件,也就是说并不是通过编译得到的 class 文件。所以这里验证其实是检查的 class 文件的内部结构是否符合字节码的要求
准备: 准备阶段的工作就是为类的静态变量分配在方法区中的内存并设为 jvm 默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为 jvm 默认的初值,而不是我们在程序中设定的初值。jvm 默认的初值是这样的:基本类型(int、long、short、char、byte、boolean、float、double)的默认值为 0。引用类型的默认值为 null。常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中 a 的初值就是 100。
解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。比如我们要在内存中找一个类里面的一个叫做 show 的方法,显然是找不到。但是在解析阶段, jvm 就会把 show 这个名字转换为指向方法区的的一块内存地址,比如 c17164,通过 c17164 就可以找到 show 这个方法具体分配在内存的哪一个区域了。这里 show 就是符号引用,而 c17164 就是直接引用。在解析阶段, jvm 会将所有的类或接口名、 字段名、 方法名转换为具体的内存地址

类的初始化:在类的生命周期执行完加载和连接之后就开始了类的初始化。在类的初始化阶段,java 虚拟机执行类的初始化语句,为类的静态变量赋值,在程序中,类的初始化有两种途径: (1)在变量的声明处赋值。(2)在静态代码块处赋值。静态变量的声明和静态代码块的初始化都可以看做静态变量的初始化,类的静态变量的初始化是有顺序的。顺序为类文件从上到下进行初始化

类加载器:
1、Java 虚拟机自带的加载器
	1)根类加载器Bootstrap ClassLoader(使用 C++编写,程序员无法在 Java 代码中获得该类)
	2)扩展加载器Extension ClassLoader,使用 Java 代码实现
	3)应用程序加载器Application ClassLoader,使用 Java 代码实现
2、用户自定义的类加载器
	java.lang.ClassLoader 的子类
	用户可以定制类的加载方式
类加载器并不需要等到某个类被“首次主动使用”时再加载它 。JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载
的过程中遇到了.class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError 错误) 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误 

 

类加载机制

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

 

1)Bootstrap ClassLoader /启动类加载器

$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

2)Extension ClassLoader/扩展类加载器

负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App ClassLoader/ 系统类加载器

负责记载classpath中指定的jar包及目录中class

4)Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader 

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

类加载双亲委派机制介绍和分析

在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。



六、基本结构:

从Java平台的逻辑结构上来看,我们可以从下图来了解JVM:

从上图能清晰看到Java平台包含的各个逻辑模块,也能了解到JDK与JRE的区别。

JVM自身的物理结构

对于JVM的学习,在我看来这么几个部分最重要:

  • Java代码编译和执行的整个过程
  • JVM内存管理及垃圾回收机制





类执行机制

JVM是基于栈的体系结构来执行class字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。