1、什么是JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
简单来说,JVM是用于执行Java应用程序和字节码的软件模块,并且可以将字节码转换为特定硬件和特定操作系统的本地代码。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行,这就是Java的能够“一次编译,到处运行”的原因。
JVM包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。
2、JRE/JDK/JVM是什么关系
JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
3、JVM体系结构
JVM的内部体系结构分为三部分(图片来自网络):
(1)类装载器(ClassLoader)子系统
用来装载.class文件
(2)执行引擎
执行字节码,或者执行本地方法
(3)运行时数据区
方法区,堆,java栈,PC寄存器,本地方法栈
4、JVM工作原理
JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种基于下层的操作系统和硬件平台并利用软件方法来实现的抽象的计算机,可以在上面执行java的字节码程序。java编译器只需面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。可以用下图来表示编译执行的简化过程:
5、JVM执行过程
1、加载class文件;
2、分配内存;
3、解释字节码成机器码;
4、运行过程垃圾收集;
5、结束。
JRE(java运行时环境)由JVM构造的java程序的运行环,也是Java程序运行的环境,但是他同时一个操作系统的一个应用程序一个进程,因此他也有他自己的运行的生命周期,也有自己的代码和数据空间。
JVM在整个jdk中处于最底层,负责于操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也就虚拟计算机。 操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境:
创建JVM装载环境和配置
装载JVM.dll
初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
调用JNIEnv实例装载并处理class类。
6、JVM的生命周期
a、两个概念
JVM实例和JVM执行引擎实例
- JVM实例对应了一个独立运行的Java程序 (进程级别)
- JVM执行引擎实例则对应了属于用户运行程序的线程 (线程级别)
b、JVM的生命周期
JVM实例的诞生
当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点
JVM实例的运行
main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。
JVM实例的消亡
当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程 序也可以使用Runtime类或者System.exit()来退出。
7、ClassLoader(类加载器)
a、JVM整个类加载过程
JVM将整个类加载过程划分为了三个步骤:
(1)装载
装载过程负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名通过ClassLoader来完成类的加载,同样,也采用以上三个元素来标识一个被加载了的类:类名+包名+ClassLoader实例ID。
(2)链接
链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口、类。在完成了校验后,JVM初始化类中的静态变量,并将其值赋为默认值。最后一步为对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备应的权限(例如public、private域权限等),会造成NoSuchMethodError、NoSuchFieldError等错误信息。
(3)初始化
初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化,在四种情况下初始化过程会被触发执行:
- 调用了new;
- 反射调用了类中的方法;
- 子类调用了初始化;
- JVM启动过程中指定的初始化类。
b、JVM类加载顺序
JVM有两种类加载器:
- 启动类装载器:是JVM实现的一部分
- 用户自定义类装载器:是Java程序的一部分,必须是ClassLoader类的子类
当JVM启动时,由Bootstrap向User-Defined方向加载类;应用进行ClassLoader时,由User-Defined向Bootstrap方向查找并加载类;
1. Bootstrap ClassLoader
这是JVM的根ClassLoader,它是用C++实现的,JVM启动时初始化此ClassLoader,并由此ClassLoader完成$JAVA_HOME中jre/lib/rt.jar(Sun JDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。
2. Extension ClassLoader
JVM用此classloader来加载扩展功能的一些jar包。
3. System ClassLoader
JVM用此classloader来加载启动参数中指定的Classpath中的jar包以及目录,在Sun JDK中ClassLoader对应的类名为AppClassLoader。
4. User-Defined ClassLoader
User-DefinedClassLoader是Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中的jar以及目录。
有关ClassLoader抽象类的几个关键方法:
loadClass
此方法负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,如没有则继续从parent ClassLoader中寻找,如仍然没找到,则从System ClassLoader中寻找,最后再调用findClass方法来寻找,如要改变类的加载顺序,则可覆盖此方法
findLoadedClass
此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native的方法。
findClass
此方法直接抛出ClassNotFoundException,因此需要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。
findSystemClass
此方法负责从System ClassLoader中寻找类,如未找到,则继续从Bootstrap ClassLoader中寻找,如仍然为找到,则返回null。
defineClass
此方法负责将二进制的字节码转换为Class对象
resolveClass
此方法负责完成Class对象的链接,如已链接过,则会直接返回。
8、执行引擎
JVM通过执行引擎来完成字节码的执行,在执行过程中JVM采用的是自己的一套指令系统,每个线程在创建后,都会产生一个程序计数器(pc)和栈(Stack),其中程序计数器中存放了下一条将要执行的指令,Stack中存放Stack Frame,表示的为当前正在执行的方法,每个方法的执行都会产生Stack Frame,Stack Frame中存放了传递给方法的参数、方法内的局部变量以及操作数栈,操作数栈用于存放指令运算的中间结果,指令负责从操作数栈中弹出参与运算的操作数,指令执行完毕后再将计算结果压回到操作数栈,当方法执行完毕后则从Stack中弹出,继续其他方法的执行。
在执行方法时JVM提供了四种指令来执行:
(1)invokestatic:调用类的static方法
(2)invokevirtual:调用对象实例的方法
(3)invokeinterface:将属性定义为接口来进行调用
(4)invokespecial:JVM对于初始化对象(Java构造器的方法为:<init>)以及调用对象实例中的私有方法时。
主要的执行技术有:解释,即时编译,自适应优化、芯片级直接执行
(1)解释属于第一代JVM,
(2)即时编译JIT属于第二代JVM,
(3)自适应优化(目前Sun的HotspotJVM采用这种技术)则吸取第一代JVM和第二代JVM的经验,采用两者结合的方式开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
9、JVM运行时数据区
PC寄存器(Program Counter Register)
(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。
如果线程正在执行的是一个Java方法,那这个计数器记录的是正在执行的字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(undefined)。 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
程序计数器是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)。
JVM栈(JVM Stacks)
每个JVM 线程都有一个私有的JVM 栈(Stacks),它将和线程同时创建。JVM 栈用来存储帧(后面会讲解)。JVM 栈类似于传统语言例如C 的栈,它持有局部变量和部分结果并且参与方法的调用和返回。 由于JVM 栈除了压入弹出帧外不会被直接操作,所以帧可以由堆(Heap)来分配。对于JVM 栈的内存不必是连续的。
JVM 规范允许JVM 栈的大小是固定的,也可以是根据需求计算来扩展和收缩。如果JVM 栈是固定大小,则每个JVM 栈大小可以在栈创建时独立地选择。一个JVM 实现可以让程序员或用户控制JVM 初始栈的大小,以及在动态扩展或收缩JVM 栈时,控制其最大值和最小值。
以下异常情况常与JVM 栈有关:
如果线程中的计算需要一个比允许的JVM 栈更大时,JVM 将会抛出StackOverflowError.
如果JVM 栈可动态扩展,当没有足够的内存分配给所尝试的扩展,或者没有足够的内存来为一个新线程创建初始化JVM 栈,JVM 将会抛出OutOfMemoryError.
堆(Heap)
JVM 有一个所有JVM 线程间共享的堆(Heap)。堆是分配所有类实例和数组内存的运行期数据区域。 堆在虚拟机启动时被创建。堆中对象的存储由自动存储管理系统(常被称为垃圾回收器或GC)回收,对象从来不会被显示的回收。JVM 承担着非特殊类型的自动存储管理系统,当然存储管理技术也可以根据实现者的系统要求来选择。堆可以是固定大小或是根据需求计算进行扩展,或者也可以是当一个大的堆不必要时进行收缩。堆的内存不需要是连续的。
一个JVM 实现可以让开发者或者用户控制堆初始的大小,同样的,如果堆能够动态扩展或者收缩,可以控制其最大值和最小值。
以下异常情况常与堆有关:如果计算需求所须更多的堆无法由自动存储管理系统提供时,JVM 将会抛出OutOfMemoryError.
方法区域(Method Area)
(1)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,可见方法区域的重要性,同样,方法区域也是全局共享的,在一定的条件下它也会被GC;当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
(2)方法区在虚拟机启动时被创建。虽然方法区逻辑上是堆的一部分,但是简单的实现可以选择既不垃圾回收也不压缩它。该版本的JVM 规范不要求指定方法区的位置或者用于管理编译后代码的策略。方法区可以是固定大小,也可以根据需求计算扩展,并且当大的方法区不再需要时进行收缩。方法区的内存不需要是连续的。 一个JVM 实现可以让开发者或用户控制方法区初始的大小,同样的,在可变大小方法区时,控制方法区的最大值和最小值。在Sun JDK中这块区域对应的为Permanet Generation,又称为持久代,默认为64M,可通过-XX:PermSize以及-XX:MaxPermSize来指定其大小。
运行时常量池(Runtime Constant Pool)
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
以下异常情况常与类或接口的常量池有关:当创建类或接口时,如果常量池的建立需要的内存不能被JVM 的方法区分配,JVM 会抛出OutOfMenoryError.
本地方法堆栈(Native Method Stacks)
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常类似,它们之间的区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由的实现它。
以下异常情况常与本地方法栈有关:
如果线程中计算所需的本地方法栈大于允许范围,JVM 会抛出StackOverflowError。
如果本地方法栈能动态扩展,当没有足够的内存分配给所尝试的扩展,或者没有足够的内存分配给新线程中创建的初始本地方法栈,JVM 就会抛出OutOfMemoryError。
与虚拟机栈一样,本地方法栈也是线程私有的。