深入了解Java虚拟机

Java虚拟机的作用

Java虚拟机的主要任务是装载class文件并且执行其中的字节码:Java虚拟机中包含一个类装载器,它可以从程序和API中装载class文件,字节码由执行引擎来执行。

不同执行引擎的Java虚拟机

1、最简单的:一次性解释字节码(直接解释字节码的Java虚拟机被称为Java解释器,解释是一种我们所知道的易于实现而执行缓慢的特殊技术)

2、即时编译器:将第一次被执行的字节码编译成本地机器代码,编译出的本地机器代码会被缓存,当方法以后被调用的时候可以重用(更快,更消耗内存)

3、自适应优化器:虚拟机开始的时候解释字节码,但是会监视运行中程序的活动,并且记录下使用最频繁的代码段。程序运行的时候,虚拟机只把那些活动最频繁的代码编译成本地代码,其他的代码由于使用得并不是很频繁,继续保留为字节码

4、最后一种虚拟机由硬件芯片构成,它用本地方法执行Java字节码,这种执行引擎实际上是内嵌在芯片里的

一个Java应用程序可以使用两种类装载器:启动类装载器和用户定义的类装载器。

由不同的类装载器装载的类将被放在虚拟机内部的不同命名空间中。

启动类装载器(系统中唯一)是Java虚拟机实现的一部分,也被称为原始类装载器、系统类装载器或者默认类装载器。

Java应用程序能够在运行时安装用户定义的类装载器,这种类装载器能够使用自定义的方式来装载类,能够用java编写,能够被编译成class文件,能够被虚拟机装载,还能够像其他对象一样实例化。它们实际上只是运行中Java应用程序可执行代码的一部分。

扫描二维码关注公众号,回复: 2221712 查看本文章

每一个类被装载的时候,Java虚拟机都监视这个类,看它到底是被启动类装载器还是被用户定义类装载器装载。当被装载的类引用了另外一个类时,虚拟机就会使用装载第一个类的类装载器装载被引用的类。

当Java虚拟机是由主机操作系统上的软件实现的时候,Java程序通过调用本地方法和主机交互。

Java中有两种方法:Java方法和本地方法。本地方法是由其他语言编写的,编译成和处理器相关的机器代码。本地方法保存在动态连接库中,格式是各个平台专有的。运行中的Java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。通过本地方法,Java程序可以直接访问底层操作系统的资源。

Java class文件

Java class文件为Java程序提供独立于底层主机平台的二进制形式的服务,这正是Java虚拟机所期待实现的。C或者C++等语言写的程序通常首先被编译,然后被连接成为单独的、专门支持特定硬件平台和操作系统的二进制文件。而Java class文件是可以运行在任何支持Java虚拟机的硬件平台和操作系统上的二进制文件。

Java虚拟机是什么

可能会是:1、抽象规范

          2、一个具体的实现

          3、一个运行中虚拟机实例

Java虚拟机抽象规范仅仅是个概念,而该规范的具体实现可能来自多个提供商,并存在于多个平台上,它或者完全用软件实现,或者以软件和硬件相结合的方式来实现。

当运行一个Java程序的同时,也就在运行了一个Java虚拟机实例,每个Java程序都运行于它自己的Java虚拟机实例中。

Java程序初始类中的main()方法,将作为该程序初始线程的起点,任何其他的线程都是由这个初始线程启动的。

Java虚拟机内部有两种线程:守护线程与非守护线程。守护线程通常是由虚拟机自己使用的,比如执行垃圾任务的线程。但是,Java程序也可以把它创建的任何线程标记为守护线程。而Java程序中的初始线程,是非守护线程。当程序中所有的非守护线程都终止时,虚拟机实例将自动退出。

当Java虚拟机运行一个程序时,它需要内存来存储许多东西,Java虚拟机把这些东西都组织到几个“运行时数据区”中,以便于管理。

某些运行时数据区是由程序中所有线程共享的,还有一些则只能由一个线程拥有,每个Java虚拟机实例都有一个方法区以及一个堆,他们是由该虚拟机实例中所有线程共享的。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息。然后,它把这些类型信息放到方法区。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。

当每一个新线程被创建时,它都将得到自己的PC寄存器和一个Java栈,如果线程正在执行的是一个Java方法,那么PC寄存器的值总是指示下一条将被执行的指令,而它的Java栈则总是存储该线程中Java方法调用的状态,而本地方法调用的状态,则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或者其他某些与实现相关的内存区中。

Java栈是由许多栈帧组成的,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中,当该方法返回时,这个栈帧被从Java栈中弹出并抛弃。

Java虚拟机为每个线程创建的内存区是私有的,任何线程都不可能访问另一个线程的PC寄存器或者Java栈

虚拟机中的数据类型

Java虚拟机是通过某些数据类型来执行计算的,数据类型分为两类:基本类型和引用类型

Java语言中的所有基本类型同样也是Java虚拟机中的基本类型,但是当编译器把Java源码编译成字节码时,它会把int或byte来表示boolean。在Java虚拟机中,false是由整数零来表示的,所有非零整数都表示true,涉及boolean值的操作则会用int。

Java虚拟机的引用类型被统称为“引用”,有三种引用类型:类类型、接口类型以及数组类型,他们的值都是对动态创建对象的引用。

在Java虚拟机中,数组是个真正的对象,还有一种特殊的引用值是null,它表示该引用变量没有引用任何对象。

类装载器子系统

在Java虚拟机中,负责查找并装载类型的那部分被称为类装载子系统。

类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。

这些动作必须严格按以下顺序进行:

1.装载:查找并装载类型的二进制数据

2.连接:执行验证,准备,以及解析(可选)

  验证:确保被导入类型的正确性

  准备:为类变量分配内存,并将其初始化为默认值

  解析:把类型中的符号引用转换为直接引用

3.初始化:把类变量初始化为正确初始值

方法区:存储被装载类型的信息

当Java虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件(线性二进制数据流)将它传输到虚拟机中,紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。该类型中的静态变量同样也是存储在方法区中。

当虚拟机运行Java程序时,它会查找使用存储在方法区中的类型信息。

由于所有线程都共享方法区,因此他们对方法区数据的访问都必须设计为线程安全的。比如,假设同时有两个线程企图访问同一个类,而这个类还没有被装入虚拟机,那么,这时只应该有一个线程去装载它,而另一个线程只能等待。

方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同样,方法区也不必是连续的,方法区可以在一个堆中自由分配。另外,虚拟机也可以允许用户指定方法区的初始大小以及最大最小尺寸。

对每个被装载的类型,虚拟机都会在方法区中存储以下类型信息

1.这个类型的全限定名

2.这个类型的直接超类的全限定名

3.这个类型是类类型还是接口类型

4.这个类型的访问修饰符

5.任何直接超接口的全限定名的有序列表

在Java class文件和虚拟机中,类型名总是以全限定名出现。在Java源代码中,全限定名由类所属包的名称加一个“.”再加上类名组成(java.lang.Object),但在class文件里,所有的“.”都被斜杠“/”代替,这样就成为java/lang/Object。

除了上面列出的基本类型信息外,虚拟机还得为每个被装载的类型存储以下信息

1.该类型的常量池

常量池就是该类型所用常量的一个有序集合,包括直接常量和对其他类型、字段和方法的符号引用,池中的数据项就像数组一样是通过索引访问的。

2.字段信息

字段名、字段的类型、字段的修饰符

3.方法信息

方法名、方法的返回值类型、方法参数的数量和类型、方法的修饰符

如果某个方法不是抽象的和本地的,它还必须保存方法的字节码、操作数栈和该方法的栈帧中的局部变量区的大小以及异常表。

4.除了常量以外的所有静态变量

5.一个到类ClassLoader的引用

如果这个类型是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。

6.一个到Class类的引用

对于每一个被装载的类型,虚拟机都会相应地为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来。Class类中的一个静态方法(forName)可以让用户得到任何已装载的类的Class实例的引用。

Java程序在运行时创建的所有类实例或数组都放在同一个堆中,而一个Java虚拟机实例中只存在一个堆空间,因此所有线程都将共享这个堆。和方法区一样,堆空间也不必是连续的内存区。在程序运行时,它可以动态扩展或收缩。事实上,一个实现的方法区可以在堆顶实现,就是当虚拟机需要为一个新装载的类分配内存时,类型信息和实际对象可以都在同一个堆上。

Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须能够快速地定位对象实例的数据。另外,它也必须能通过该对象引用访问相应的类数据(存储于方法区的类型信息)。因此在对象中通常会有一个指向方法区的指针。

程序计数器

对于一个运行中的Java程序而言,其中的每一个线程都有它自己的PC寄存器(程序计数器),它是在该线程启动时创建的,PC寄存器的大小是一个字长,因此它能够持有一个本地指针,也能够持有一个returnAddress。

Java栈

每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。虚拟机只会对Java栈执行两种操作:以帧为单位的压栈和出栈。

栈帧

栈帧由三部分组成:局部变量区,操作数栈和帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。编译器在编译时就确定了这些值并放在class文件中。而帧数据区的大小依赖于具体的实现。

当虚拟机调用一个Java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入Java栈中。

本地方法栈

对于一个运行中的Java程序而言,它还可能会用到一些跟本地方法相关的数据区。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但不止于此。任何本地方法接口都会使用某种本地方法栈。当线程调用本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法,可以把这看做是虚拟机利用本地方法来动态扩展自己。

执行引擎

任何Java虚拟机实现的核心都是它的执行引擎。在Java虚拟机规范中,执行引擎的行为使用指令集来定义。

指令集

方法的字节码流是由Java虚拟机的指令序列构成的,每一个指令包含一个单字节的操作码,后面跟随0个或多个操作数。操作码表明需要执行的操作,操作数向Java虚拟机提供执行操作码需要的额外信息。操作码本身就已经规定了它是否需要跟随操作数,以及如果有操作数的话,它是什么形式的。很多Java虚拟机的指令不包含操作数,仅仅是由一个操作码字节构成的。

猜你喜欢

转载自blog.csdn.net/qq_40844628/article/details/81082750