深刻Java虚拟机读书笔记第五章Java虚拟机

Java虚拟机

  Java虚拟机之因此被称之为是虚拟的,就是由于它仅仅是由一个规范来定义的抽象计算机。所以,要运行某个Java程序,首先须要一个符合该规范的具体实现。java

Java虚拟机的生命周期

  一个运行时的Java虚拟机实例的天职就是:负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。每一个Java程序都运行于它本身的Java虚拟机实例中。程序员

  Java虚拟机实例经过调用某个初始类的main()方法来运行一个Java程序。而这个main()方法必须是共有的public、静态的static、返回值为void,而且接受一个字符串数组做为参数。任何拥有这样一个main()方法的类均可以做为Java程序运行的起点。算法

好比,考虑这样一个Java程序,它打印出传给它的命令行参数:数据库

Class Echo{数组

  Public static void main(String[] args){安全

    Int len = args.length;数据结构

    For(int i = 0; i < len; ++i){多线程

      System.out.print(args[i] + “ ”);数据结构和算法

    }函数

    System.out.println();

  }

}

  必须告诉Java虚拟机要运行的Java程序中初始类的名字,这个程序将从它的main()方法开始运行。如在Windows上使用命令:

Java Echo Greetings, Planet.

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

  在Java虚拟机内部有两种线程:守护线程和非守护线程。守护线程一般是由虚拟机本身用的,好比执行垃圾收集任务的线程。可是,Java程序也能够把它建立的任何线程标记为守护线程。而Java程序中的初始线程---就是开始于main()的那个,是非守护线程。

  只要还有任何非守护线程在运行,那么这个Java程序也在继续运行(虚拟机仍然存活)。当该程序全部的非守护线程都终止时,虚拟机实例将自动退出。倘若安全管理器运行,程序自己也可以经过调用Runtime类或者System类的exit()方法退出。

Java虚拟机的体系结构

  Java虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型以及指令这几个术语来描述的

  下图为Java虚拟机的结构框图,包括在规范中描述的主要子系统和内存区。前面提到,每一个Java虚拟机都有一个类装载器子系统,它根据给定的全限定名来装入类型。一样,每一个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。

 

 

  当Java虚拟机运做一个程序时,它须要内存来存储许多东西,例如,字节码,从已装载的class文件中获得的其余信息,程序建立的对象、传递给方法的参数、返回值、局部变量以及运算的中间结果等,Java虚拟机把这些东西都组织到几个“运行时数据区”中,以便于管理。Java虚拟机规范对“运行时数据区”的描述是抽象的,由具体实现的设计者决定

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

  当每个新线程被建立时,它都将获得它本身的PC寄存器(程序计数器)以及一个Java:若是线程正在执行的是一个Java方法(非本地方法),那么PC寄存器的值将老是指示下一条将被执行的指令,而它的Java栈则老是存储该线程中Java方法调用的装载---包括它的局部变量,被调用时传进来的参数,它的返回值,以及运算的中间结果等。而本地方法调用的状态,则是以某种依赖于具体实现的方式存储在本地方法栈中,也多是在寄存器或者其余某些与特定实现相关的内存中。

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

  Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的缘由是为了保持Java虚拟机的指令集尽可能紧凑,同时也便于Java虚拟机在那些只有不多通用寄存器的平台上实现。另外Java虚拟机的这种基于栈的体系结构也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。

  下图描绘了Java虚拟机为每个线程建立的内存区,这些内存区域是私有的,任何线程都不能访问另外一个线程的PC寄存器或者Java栈。

 

 数据类型

    Java虚拟机是经过某些数据类型来执行计算的,数据类型及其运算都是由Java虚拟机规范严格定义的,数据类型能够分为两种:基本类型和引用类型。基本类型的变量持有原始值,而引用类型的变量持有引用值。术语“引用值”指的是对某个对象的引用,而不是该对象的自己,与此相对,原始值则是真正的原始数据。

 

  Java语言中的全部基本类型一样也都是Java虚拟机中的基本类型。可是boolean有点特别,虽然Java虚拟机也把boolean看作基本类型,可是指令集对boolean只有颇有限的支持。当编译器把Java源码编译为字节码时,它会用intbyte来表示boolean。在Java虚拟机中,false是由整数零来表示的,全部非零整数都表示true,涉及boolean值的操做则会使用int。另外,boolean数组是当作byte数组来访问的,可是在“堆”区,它也能够被表示为位域。

  Java虚拟机中还有一个只在内部使用的基本类型:returnAddressJava程序猿不能使用这个类型,这个基本类型被用来实现Java程序的finally字句。

  Java虚拟机的引用类型被统称为“引用”(reference),有三种引用类型:类类型、接口类型以及数组类型,它们的值都是对动态建立对象的引用。类类型的值是对类实例的引用;数组类型的值是对数组对象的引用,在Java虚拟机中,数组是个真正的对象;而接口类型的值,则是对实现了该接口的某个类实例的引用。

  Java虚拟机规范定义了每一种数据类型的取值范围,可是却没有定义它们的位宽。位宽由具体的虚拟机实现设计者决定

字长的考量

  Java虚拟机中,最基本的数据单元就是字,它的大小是由每一个虚拟机实现的设计者来决定的。字长必须足够大,至少是一个字单元就足以持有byteshortintcharfloatreturnAddress或者reference类型的值,而两个字单元就足以持有long或者double类型的值。所以,虚拟机实现的设计者至少得选择32位做为字长,或者选择更为高效的字长大小。一般根据底层主机平台的指针长度来选择字长。

  在Java虚拟机规范中,关于运行时数据区的大部份内容,都是基于“字”这个抽象概念的。好比,关于栈帧的两个部分---局部变量和操做数栈---都是按照“字”来定义的。这些内存区可以容纳任何虚拟机数据类型的值,当把这些值放到局部变量或者操做数栈中时,它将占用一个或两个字单元。

  在运行时,Java程序没法侦测到底层虚拟机的字长大小;一样,虚拟机的字长大小也不会影响程序的行为---它仅仅是虚拟机实现的内部属性。

 

类装载子系统

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

  Java虚拟机有两种类装载器:启动类装载器和用户自定义类装载器。前者是Java虚拟机实现的一部分,后者则是Java程序的一部分。由不一样的类装载器装载的类将被放在虚拟机内部的不一样命名空间中。

  类装载子系统涉及Java虚拟机的其余几个组成部分,以及几个来自java.lang库的类。好比,用户自定义的类装载器是普通的Java对象,它的类必须派生自java.lang.ClassLoader类。ClassLoader中定义的方法为程序提供了访问类装载器机制的接口。此外,对于每个被装载的类型,Java虚拟机都会为它建立一个java.lang.Class类的实例来表明该类型。和全部其余对象同样,用户自定义的类装载器以及Class类的实例都放在内存中的堆区,而装载的类型信息则都位于方法区

  装载、链接以及初始化  类装载子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。这些动做必须严格按如下顺序进行

一、装载---查找并装载类型的二进制数据

二、链接---执行验证,准备,以及解析

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

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

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

三、初始化---把类变量初始化为正确初始值

  启动类装载器  只要是符合Java class文件格式的二进制文件,Java虚拟机实现都必须可以从中辨别并装载其中的类和接口。某些虚拟机实现也能够识别其余的非规范的二进制格式文件,但它必须可以辨别class文件。

  每一个Java虚拟机实现都必须有一个启动类装载器,它知道怎么装载受信任的类,好比Java APIclass文件Java虚拟机规范并未规定启动类装载器如何去寻找class文件。

  只要给定某个类型的全限定名,启动类装载器就必须可以以某种方法获得定义该类型的数据。在JDK1.2中,启动类装载器只在系统类(Java API的类文件)的安装路径中查找要装入的类;而搜索CLASSPATH目录的任务,如今交给了系统类装载器---它是一个自定义的类装载器,当虚拟机启动时就被自动建立。

  用户自定义类装载器    尽管“用户自定义类装载器”自己是Java程序的一部分,但类ClassLoader中的四个方法是通往Java虚拟机的通道:

  protected final class defineClassString name, byte data[], int offset, int length

  protected final class defineClassString name, byte data[], int offset, int length, ProtectionDomain protectionDomain

  protected final Class findSystemClass(String name);

  protected final void resolveClass(Class c);

  任何Java虚拟机实现都必须把这些方法链接到内部的类装载器子系统中。

  两个被重载的defineClass()方法都要接受一个名为data[]的字节数组做为输入参数,而且在data[offset]data[offset+length]之间的二进制数据必须符合Java class文件格式---它表示一个新的可用类型。而name参数是个字符串,它给出指定类型的全限定名。使用第一个defineClass()时,该类型被赋以默认的保护域,使用第二个时该类型的保护域由它的protectionDomain参数指定。每一个Java虚拟机实现都必须保证ClassLoader类的defineClass()方法可以把新类型导入到方法区中。

  findSystemClass()方法接受一个字符串做为参数,它指出被装入类型的全限定名。在版本1.2中,该方法使用系统类装载器来装载指定类型。任何Java虚拟机实现都必须保证findSystemClass()方法可以以这种方式调用系统类装载器。

  resolveClass()方法接受一个Class实例的引用做为参数,它将对该Class实例表示的类型执行链接动做。而defineClass()方法则只负责装载。defineClass方法返回一个Class实例时,也就表示指定的class文件已经被找到并装载到方法区了,可是却不必定被链接和初始化了。Java虚拟机实现必须保证ClassLoader类的resolveClass方法可以让类装载器子系统执行链接动做

  命名空间  个类装载器都有本身的命名空间,其中维护着由它装载的类型。一个Java程序能够屡次装载具备同一个全限定名的多个类型,当多个类装载器都装载了同名的类型时,为了惟一地标识该类型,还要在类型名称前加上装载该类型的类装载器的标识

  Java虚拟机中的命名空间,实际上是解析过程的结果。对于每个被装载的类型,Java虚拟机都会记录装载它的类装载器。当虚拟机解析一个类到另外一个类的符号引用时,它须要被引用类的类装载器。

方法区

    在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上被称为方法区的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,而后读人这个class文件---一个线性二进制数据流---而后将它传输到虚拟机中、.紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。该类型中的类(静态)变量一样也是存储在方法区。Java虚拟机在内部如何存储类型信息,这是由具体实现的设计者来决定的。

  当虚拟机运行Java程序时,它会查找使用存储在方法区中的类型信息。设计其应当为类型信息的内部表示设计适当的数据结构,以尽量在保持虚拟机小巧紧凑的同时加快程序的运行效率。若是正在设计一个须要在少许内存的限制中操做的实现,设计者可能会决定以牺牲某些运行速度来换取紧凑性。另一方面,若是设计一个将在虚拟内存系统中运行的实现,设计者可能会决定在方法区中保存一些冗余倍息,以此来加快执行速度。(若是底层主机没有提供虚拟内存,可是提供了一个硬盘,设计者可能会在实现中建立一个虚拟内存系统。Java虛拟机的设计者能够根据目标平台的资源限制和需求,在空问和时间上作出权衡.选择实现什么样的数据结构和数据组织。

  因为全部线程都共享方法区,所以它们对方法区数据的访问必须被设计为是线程安全的。好比,假设同时有两个线程都企图访问一个名为Lava的类,而这个类尚未被装人虚拟机,那么,这时只应该有一个线程去装载它,而另外一个线程则只能等待。方法区的大小没必要是固定的,虚拟机能够根据应用的须要动态调整。一样,方法区也没必要是连续的,方法区能够在一个堆(甚至是虚拟机本身的堆)中自由分配。另外,虚拟机也能够容许用户或者程序员指定方法区的初始大小以及最小和最大尺寸等。

  方法区也能够被垃圾收集,由于虚拟机容许经过用户定义的类装载器来动态扩展Java程序,所以一些类也会成为程序“再也不引用”的类。当某个类变为再也不被引用的类时,Java虚拟机能够卸载这个类(垃圾收集)从而使方法区占据的内存保持最小。

  类型信息  对每一个装栽的类型,虚拟机都会在方法区中存储如下类型信息:

    •这个类型的全限定名。

    •这个类型的直接超类的全限定名(除非这个类型是java.lang.Object,它没有超类)

    •这个类型是类类型仍是接口类型

    •这个类型的访问修饰符(public、abstract或final的某个子集)

    •任何直接超接口的全限定名的有序列表。

  在Java class文件和虚拟机中,类型名老是以全限定名出如今Java源代妈中,全限定名由类所属包的名称加一个再加一个“.”,再加上类名组成。例如,类Object的所属包为java.lang,那它的全限定名应该是java.lang.Object,但在class文件里,全部的“.”都被斜杠“/”代替.这样就成为java/lang/Objectc。至于全限定名在方法区中的表示,则因不一样的设计者有不一样的选择而不一样,能够用任何形式和数据结构来表明。

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

    •该类型的常量池。

    •字段信息。

    •方法信息

    •除了常量之外的全部类(静态)变量。

    •一个到类ClassLoader的引用。

    •一个到Class类的引用。

  常量池  虚拟机必须为每一个被装载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,包括直接常量(string、integer和floating point常量)和对其余类型、字段和方法的符号引用。池中的数据项就像数组同样是经过索引访问的。由于常量池存储了相应类型所用到的全部类型、字段和方法的符号引用,因此它在Java程序的动态链接中起着核心的做用

  字段信息    对于类型中声明的每个字段,方法区中必须保存下面的信息。除此以外,这些字段在类或接口中的声明顺序也必须保存。下面是字段信息的清单:

  •字段名。

  •字段的类型。

  •字段的修饰符(publicprivateprotected.staticfinalvolatiletransient的某个子集)。

  方法信息    对于类型中声明的每个方法,方法区中必须保存下面的信息。和字段同样,这些方法在类或者接口中的声明顺序也必须保存。下面是力法信息的清单:

  •方法名。

  •方法的返回类型(void)

  •方法参数的数量和类型(按声明顺序).

  •方法的修饰符(publicprivateprotectedstatic, findsynchronizednativeabstract的某个子集)

除上面的清单中列出的条目以外,若是某个方法不是抽象的和本地的,它还必须保存下列信息:

  •方法的字节码(bytecodes)

  •操做数栈和该方法的栈帧中的局部变量区的大小。

  •异常表。

  类(静态)变量类变量是由全部类实例共享的,可是即便没有任何类实例,它也能够被访问。这些变量只与类有关——而非类的实例,所以它们老是做为类型信息的一部分而存储在方法区。除了在类中声明的编译时常量外,虚拟机在使用某个类以前,必须在方法区中为这些类变量分配空间。

  而编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量)则和通常的类变量的处理方式不一样,每一个使用编译时常量的类型都会复制它的全部常量到本身的常量池中,或嵌人到它的字节码流中。做为常量池或字节码流的一部分,编译时常量保存在方法区中——就和通常的类变量同样。可是当通常的类变量做为声明它们的类型的一部分数据面保存的时候,编译时常量做为使用它们的类型的一部分而保存。

    指向ClassLoader类的引用    每一个类型被装载的时候,虚拟机必须跟踪它是由启动类装载器仍是由用户自定义类装载器装载的。若是是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。这是做为方法表中的类型数据的一部分保存的。

  虚拟机会在动态链接期间使用这个信息。当某个类型引用另外一个类型的时候,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型。这个动态链接的过程,对于虚拟机分离命名空间的方式也是相当重要的。为了可以正确地执行动态链接以及维护多个命名空间,虚拟机须要在方法表中得知每一个类都是由哪一个类装载器装载的。

  指向Class类的引用    对于每个被装载的类型(无论是类仍是接口)虚拟机都会相应地为它建立一个java.lang.Class类的实例,并且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来。在你的Java程序中,你能够获得并使用指向Class对象的引用。Class类中的一个静态方法可让用户获得任何己装载的类的Class实例的引用。

  public static Class forName(String classHame)  //链接数据库经常使用此方法

  好比,若是调用forName("java.lang.Object"),那么将获得一个表明java.lang.Object的Class对象的引用。若是调用forName("java.util.Enumeration"),那么获得的是表明java.util包中java.util.Enumeration接口的Class对象的引。可使用forName()来获得表明任何包中任何类型的Class对象的引用,只要这个类型能够被(或者已经被)装载到当前命名空间中。若是虚拟机没法把请求的类型装载到当前命名空间,那么forName ()会抛出ClassNotFoundException异常。

  另外一个获得Class对象引用的方法是,能够调用任何对象引用的getClass()方法。这个方法被来自Object类自己的全部对象继承:

Public final class getClass();

   好比,若是你有一个到java.lang.Integer类的对象的引用,那么你只需简单地调用Integer对象引用的getClass()方法,就能够获得表不java.lang,Integer类的Class对象。给出一个指向Class对象的引用,就能够经过Class类中定义的方法来找出这个类型的相关信息。若是查看这些方法,会很快意识到,Class类使得运行程序能够访问方法区中保存的信息。

  下面是Class类中生明的方法:

  public String getNameO;

  public Class getSuperClass();

  public boolean islnterface();

  public Class[] getlnterface();

  public ClassLoader getClassLoader ();

  这些方法仅能返回已装载类型的信息。getName()返回类型的全限定名,getSuperChss()返回类型的直接超类的Class实例。若是类型是java.lang.Object类或者是一个接口,它们都没有超类,getSuperClass()返回null。Islntcrface()判断该类型是不是接口,若是Class对象描述一个接口就返回true;若是它描述一个类则返回false。getlnterfaces()返回一个Class对象数组,其中每一个Class对象对应一个直接超接口,超接口在数组中以类型声明超接口的顺序出现。若是该类型没有直接超接口,getlnterfaces()则返回一个长度为零的数纽。getClassLoader()返回装载该类型的ClassLoadeT对象的引用,若是类型是由启动类装载器装载的,则返回null。全部这些信息都直接从方法区中得到。

  方法表    为了尽量提升访问效率,设计者必须仔细设计存储在方法区中的类型信息的数据结构,所以,除了以上讨论的原始类型信息,实现中还可能包括其余数据结构以加快访问原始数据的速度,好比方法表。虚拟机对每一个装载的非抽象类,都生成一个方法表,把它做为类信息的一部分保存在方法表中。方法表是一个数组,它的元素是全部它的实例可能被调用的实例方法的直接引用,包括那些从超类继承过来的实例方法。(对于抽象类和接口,方法表没有什么帮助,由于程序决不会生成它们的实例。)运行时能够经过方法表快速搜寻在对象中调用的实例方法。

  方法区使用示例,为了展现虚拟机如何便用方法表中的信息,咱们举个例子,看下面这个类:

Class Lava {

Private int speed = 5;

Void flow() {

  }

}

 

Class Volcano{

  Lava lava = new Lava();

  lava.flow();

}

  下面的段落描述了某个实现中是如何执行Volcano程序中main()方法的字节码中第一条指令的。不一样的虚拟机实现可能会用彻底不一样的方法来操做,下面描述的只是其中一种可能——但并非仅有的一种,下面看一下Java虚拟机是如何执行Volcano程序中main ()方法的第一条指令的。

  要运行Vokano程序,首先得以某种“依赖于实现的”方式告诉虚拟机“Volcano”这个名字。以后,虚拟机将找到并读人相应的class文件“Volcano.class”,而后它会从导人的class文件里的二进制数据中提取类型信息并放到方法区中。经过执行保存在方法区中的字节码,虚拟机开始执行main()方法,在执行时,它会一直持有指向当前类(Volcano类)的常量池(方法区中的一个数据结构)的指针。

  注意,虚拟机开始执Volcano类中main()方法的字节码的时候,尽管Lava类还没被装载,可是和大多数(.也许全部)虚拟机实现同样,它不会等到把程序中用到的全部类都装载后才开始运行程序。刚好相反,它在须要时才装载相应的类。main()的第一条指令告知虚拟机为列在常量池第一项的类分配足够的内存。因此虚拟机使用指向Volcano常量池的指针找到第一项,发现它是一个对Lava类的符号引用,而后它就检查方法区,看Lava类是否已经被装载了。

  这个符导引用仅仅是一个给出类Lava的全限定名“Lava”的字符串。为了能让虚拟机尽量快地从一个名称找到类,设计者应当选择最佳的数据结构和算法。这里能够采用各类方法,如散列表,搜索树等等。一样的算法也能够用于实现Class类的forName()方法,这个方法根据给定的全限定名返同Class引用。

  当虚拟机发现尚未装载过名为“Lava”的类时,它就开始査找并装载文件“Lava.class”,并把从读人的二逬制数据中提取的类型信息放在方法区中。紧接着,虚拟机以一个直接指向方法区Lava类数据的指针来替换常量池第一项(就是那个字符串“Lava”)——之后就能够用这个指针来快速地访问Lava。这个替换过挥称为常量池解析,即把常量池中的符号引用替换为直接引用。这是逋过在方法区中搜索被引用的元素实现的,在这期间可能又须要装载其余类。在这里,咱们替换掉符号引用的“直接引用”是一个本地指针。

  终于,虚拟机准备为一个新的Lava对象分配内存。此吋,它又须要方法区中的信息。还记得刚刚放到Volcano类常量池第一项的指针吗?如今虚拟机用它来访问Lava类型倍息(此前刚放到方法区中的),找出其中记录的这样一个信息:一个Lava对象须要分配多少堆空间。

  Java虚拟机总可以经过存储于方法区的类型信息来肯定一个对象须要多少内存,可是,某个特定对象事实上须要多少内存,是跟特定实现相关的。对象在虚拟机内部的表示是由实现的设计者来决定的。

  Java虛拟机肯定了一个Lava对象的大小后,它就在堆上分配这么大的空间,并把这个对象实例的变量speed切始化为默认初始值0。假如Lava类的超类Object也有实例变量,则也会在此时被初始化力相应的默认值。

  当把新生成的Lava对象的引用压到栈中,main()方法的第—条指令也完成了。接下来的指令经过这个引用调用Java代码(该代码把speed变量初始化为正确初始值5)。另一条指令将用这个引用调用Lava对象引用的flow()方法。

  Java程序在运行时建立的全部类实例或数组都放在同一个堆中。而一个Java虚拟机实例中只存在一个堆空间,所以全部线程都将共享这个堆。又因为一个Java程序独占一个Java虚拟机实例,于是每一个Java程序都有它本身的堆空间——它们不会彼此干扰。可是同一个Java程序的多个线程却共享着同一个堆空间,在这种状况下,就得考虑多线程访问对象(堆数据)的同步问题了。

  Java虚拟机有一条在堆中分配新对象的指令,却没存释放内存的指令。正如你没法用Java代码去明确释放一个对象同样,字节码指令也没有对应的功能。虚拟机本身负责决定如何以及什么时候释放再也不被运行的程序引用的对象所占据的内存。程序自己不用去考虑什么时候需回收对象所占用的内存,一般,虚拟机把这个任务交给垃圾收集器。

  垃圾收集    垃圾收集器的主要工做就是自动回收再也不被运行的程序引用的对象所占用的内存。此外,它也可能去移动那些还在使用的对象,以此减小堆碎片。

  Java虛拟机规范并无强制规定垃圾收集器,它只要求虚拟机实现必须“以某种方式”管理本身的堆空间。举个例子,某个实现可能只有固定大小的堆空问可用,当空间填满,它就简单地拋出OutOfMemory异常,根本不去考虑回收垃圾对象的问题。这样的一个实现虽然简陋,担倒是符合规范的。总之,Java虚拟机规范并无规定具体的实现必须为Java程序准备多少内存,也没有说它必须怎么管理自已的堆空间,它仅仅告诉实现的投计者:Java稈序须要从堆中为对象分配空间,而且程序自己不会主动释放它。所以堆空间的管理(包括垃圾收集)问题得由设计者自行去考虑处理方式。

  Java虚拟机规范没有指定垃圾收集应该采用什么技术。这些都由虚拟机的设计者根据他们的目标、考虑所受的限制、用本身的能力去决定什么才是最好的技术。由于到对象的引用可能不少地方都存在,如Java栈、堆、方法区、本地方法栈,因此垃圾收集技术的使用在很大程度上会影响到运行时数据区的设计。

  和方法区同样,堆空间也没必要是连续的内存区。在程序运行时,它能够动态扩展或收缩。事实上,一个实现的方法区能够在堆顶实现。换句话说,就是虚拟机须要为一个新装载的类分配内存时,类型信息和实际对象能够都在同一个堆上。所以,负责回收无用对象的垃圾收集器可能也要负责无用类的释放(卸载)。另外,某些实现可能也容许用户或程序员指定堆的初始大小、最大最小值等等。

  对象的内部表示    Java虚拟机规范并无规定lava对象在堆中是如何表示的。对象的内部表示也影响着整个堆以及垃圾收集器的设计,它由虚拟机的实现者决定。

  Java对象中包含的基本数据由它所属的类及其全部超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须可以快速地定位对象实例的数据。另外,它也必须能经过该对象引用访问相应的类数据(存储于方法区的类型信息)。所以在对象中一般会有一个指向方法区的指针。一种可能的堆空间设计就是,把堆分为两部分:一个句栖池,一个对象池,如图5-5所示。而一个对象引用就是一个指向句栖池的本地指针。句柄池的每一个条目有两部分:一个指向对象实例变量的指针,一个指向方法区类型数据的指针。这种设计的好处是有利于堆碎片的整理,当移动对象池中的对象时,句柄部分只需耍更改一下指针指向对象的新地址就能够了——就是在句柄池中的那个指针。缺点是每次访问对象的实例变量都要舒过两次指针传递。

  另外一种设计方式是使对象指针宜接指向一组数据,而读数据包括对象实例数据以及指向方法区中类数据的指针。这样设计的优缺点正好与前面的方法相反,它只须要一个指针就能够访问对象的实例数据,可是移动对象就变得更加复杂。当使用这种堆的虚拟机为了减小内存碎片。而移动对象的时候,它必须在整个运行时数据K中更新指向被移动对象的引用。图5-6描绘了这种表示对象的方法。

  有以下几个理由要求虚拟机必须可以经过对象引用获得类(类權)数据:当程序在运行时须要转换某个对象引用为另外一种类型时,虚拟机必需要检查这种转换是否被容许,被转换的对象是否的确足被引用的对象或者它的超类型。当程序在执行instanceof操做时,虚拟机也进行了一样的检查。在这两种状况下,虚拟机部须要查看被引用的对象的类数据。最后,当程序中调用某个实例方法时,虚拟机必须迸行动态绑定,换句话说,它不能按照引用的类型来决走将要调用的方法.而必须报据对象的'实际类。为此,虚戒机必须再次经过对象的引用去访问类数据。

  无论虚拟机的实现使用什么样的对象表示法,极可能每一个对象都有一个方法表,由于方法表加快了调用实例方法时的效率,从而对Java虚拟机实现的总体性能起着很是重要的正面做用;可是Java虚拟机规范并未要求必须使用方法表,因此并是全部实现中都会使用它。好比那些有严格内存资源限制的实现,或许它们裉本不可能有足够的额外内存资源来存储方法展。若是一个实现使用方法表,那么仅仅使用一个指向对象的引用,就能够很快地访问到对象的方法表。

 

  下图展现了一种把方法表和对象引用联系起来的实现方式。每一个对象的数据都包含一个指向特殊数据结构的指针,这个数据结构位于方法区,它包括两部分:

  •一个指向方法区对应类数据的指针。

  •此对象的力法表。

  方法表是个指针数组,其中的每一项都是一个指向“实例方法数据”的指针,实例方法能够被那类的对象调用。方法表指向的实例方法数据包括如下信息:

  •此方法的操做数栈和局部变里区的大小。

  •此方法的字节码。

  •异常表。

  这些足够虚拟机去用一个方法了,方法表中包含有方法指针---指向类或其超类声明的方法的数据:也就是说,方法表所指向的方法多是此类声明的,也多是它继承下来的。

  堆上的对象数据中还有一个逻辑部分,那就是对象锁。这是—个互斥对象,虚拟机屮的每一个对象都有一个对象锁,它被用于协调多个线程访问同一个对象时的同步。在任什么时候刻,只能有一个线程“拥有”这个对象锁,所以只有这个线程才能访问该对象的数据。此时其余但愿访问这个对象的线程只能等待,直到拥有对象锁的线程释放锁。当某个线程拥有一个对象锁后,能够继续对这个锁追加请求。但请求几回,必须对应地释放几回,以后才能轮到其余线程。好比一个线程清求了三次锁,在它释放三次锁以前,它一直保持“拥有”这个锁。

 

  不少对象在其整个生命周期内都没有被任何线程加锁。在线程实际请求某个对象的锁以前,实现对象锁所须要的数据是没必要要的。不少实现不在对象自身内部保存一个指向锁数据的指针。而只有当第一次须要加锁的时候才分配对应的锁数据,但这时虚拟机要用某种间接方法来联系对象数据和对应的锁数据,例如把锁数据放在一个以对象地址为索引的搜索树中。

  除了实现锁所须要的数据外,每一个Java对象逻辑上还与实现等待集合(wait  set)的数据相关联。锁是用来实现多个线程对共享数据的互斥访问的,而等待集合是用来让多个线程为完成一个共同目标而协调工做的。

  等待集合由等待方法和通知方法联合使用。每一个类都从Object那里继承了三个等待方法(三个名为wait()的重载方法)和两个通知方法(notify()notifyAll())。当某个线程在一个对象上调用等待方法吋,虚拟机就阻塞这个线程,并把它放在了这个对象的等待集合中。直到另外一个线程在同一个对象上调用通知方法,虚拟机会在以后的某个时刻唤醒一个或多个在等待集合中被阻塞的线程。正像锁数据一样,在实际调用对象的等待方法或通知方法以前,实现对象的等待集合的数椐并非必需的。所以,许多虚拟机实现都把等待集合数据与实际对象数据分开,只有在须要时才为此对象建立同步数据(一般是在第一次调用等待方法或通知方法时)

  最后一种数据类型——能够做为堆中某个对象映像的一部分,是与拉圾收集器有关的数据。垃圾收集器必须(以某种方式)跟踪程序引用的每一个对象,这个任务不可避免地要附加一些数据给这些对象,数据的类型要视拉圾收集使用的算法而定。例如,假如垃圾收集器使用“标记并清除”算法,这就须要可以标记对象可否被引用。此外,对于再也不被引用的对象,还须要指明它的终结方法(finalize)是否已经运行过了。像线程锁同样,这些数据也能够放在对象数据外。有一些垃圾收集技术只在垃圾收集器运行时须要额外数据。例如“标记并清除”算法就使用一个独立的位图来标记对象的引用状况。

  除了标记对象的引用状况外,垃圾收集器还要区分对象是否调用了终结方法。对干在其类中声明了终结方法的对象,在回收它以前,垃圾收集器必须调用它的终结方法。Java语言规范指出,拉圾收集器对每一个对象只能调用一次终结方法,可是容许终结方法复活(resurrect)这个对象,即容许该对象被再次引用。这样当这个对象再次被回收时,就不用再调用终结方法了。须要终结方法的对象很少,而须要复活的更少,因此对一个对象回收两次的情況不多见。这种用来标志终结方法的数据虽然逻辑上是对象的一部分,但一般实现上不随对象保存在堆中。大部分状况下,垃圾收集器会在一个单独的空间保存这个信息。

  数组的内部表示  Java中,数组是真正的对象。和其余对象同样,数组老是存储在堆中。一样,和普通对象同样,实现的设计者将决定数组在堆中的表示形式。

  和其余全部对象同样,数组也拥有一个与它们的类相关联的Class实例,全部具备相同维度和类型的数组都是同一个类的实例,而无论数组的长度(多维数组每一维的长度)是多少,例如一个包含3int整数的数组和一个包含300int整数的数组拥有同一个类。数组的长度只与实例数据有关。

  数组类的名称由两部分组成:每一维用一个方括号“[”表示,用字符或字符串表示类型。好比,元素类型为int整数的、一维数组的类名为“[I”,元素类型为byte的三维数组为“[[[B”,元素类型为Object的二维数组“[[Ljava/lang/Object

  多维数组被表示为数组的数组。好比,int类型的二维数组,将表示为一个一维数组,其中的毎个元素是一个一维int数组的引用

  在堆中的每一个数组对象还必须保存的数据是数组的长度、数组数据,以及某些指向数组的类数据的引用。虚拟机必须可以经过一个数组对象的引用获得此数组的长度,经过索引访问其元素(其间要检查数组边界是否越界),调用全部数组的直接超类Object声明的方法等等。

程序计数器

  对于一个运行中的Java程序而言,其中的每个线程都有它本身的PC(程序计数器)寄存器,它是在该线程启动时建立的。PC寄存器的大小是一个字长,所以它既可以持有一个本地指针,也可以持有一个returnAddress。当线程执行某个Java方法时,PC寄存器的内容老是下一条将被执行指令的“地址”,这里的“地址”能够是一个本地指针,也能够是在方法字节码中相对于该方法起始指令的偏移量。若是该线程正在执行一个本地方法,那么此PC寄存器的值是“undefined'”。

Java

  每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。前面咱们曾经提到,Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操做:以帧为单位的压栈或出栈。某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的帧栈称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。在线程执行一个方法时,它会跟踪当前类和当前常量池。此外,当虚拟机遇到栈内操做指令时,它对当前帧内数据执行操做。

  每当线程调用一个Java方法时,虛拟机都会在该线程的Java栈中压人一个新帧。而这个新帧天然就成为了当前侦。在执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等等数据。

  Java方法能够以两种方式完成。一种经过return返回的,称为正常返回;一种是经过抛出异常而异常停止的。无论以哪一种方式返回,虚拟机都会将当前帧弹出Java栈而后释放掉,这样上—个方法的帧就成为当前帧了。

  Java栈上的全部数据都是此线程私有的。任何线程都不能访问另外一个线程的栈数据,所以咱们不须要考虑多线程状况下栈数据的访问同步问题。当一个线程调用一个方法时,方法的局部变量保存在调用线程Java找的帧中。只有一个线程能老是访问那些局部变最,即调用方法的线程。

  像方法区和堆同样,Java栈和帧在内存中也没必要是连续的。帧能够分布在连续的栈里,也能够分布在堆里,或者两者兼而有之。表示Java栈和栈帧的实际数据结构由虛拟机的实现者决定,某些实现容许用户指定Java栈的初始大小和最大最小值。

栈帧

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

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

  局部变量区  Java栈帧的局部变量区被组织为一个以字长为单位、从0开始计数的数组。字节码指令经过从0开始的索引来使用其中的数据类型。类型为intfloatreferencereturnAddress的值在数组中只占据一项,而类型为byteshortchar的值在存入数组前都将被转换为int值,于是一样占据一项。可是类型为longdouble的值在数组中却占据连续的两项。

  在访问局部变量中的longdouble值的时候,指令只需指出连续两项中第一项的索引值。例如某个long值占据第34项,那么指令会取索引为3long值。局部变量区的全部值都是字对齐的,longdouble这样占据两项数组元素的值一样能够起始于任何索引。

  局部变量区包含对应方法的参数和局部变量。编译器首先按声明的顺序把这些参数放入局部变量数组。

  除了Java方法的参数(编译器首先严格按照它们的声明顺序放到局部变量数组中,而对于真正的局部变量,它能够任意决定放置顺序,甚至能够用一个索引指代两个局部变量---好比当两个局部变量的做出域不重叠时,像下面Example3b中的局部变量ij就是这种情形:在方法的前半段,在开始生效以前,0号索引的入口能够被用来表明i。在方法的后半段,i经超过了有效做用域,0号入口就能够用来表示j了。

class Example3b{

  public static void runtwoLoops(){

  for(int i=0; i < 10; ++i){

    System.out.println(i);  

  }

  for(int j=9; j >=0; --j){

    System.out.println(j);  

  }

  }

}

  和其余运行时内存区同样,虚拟机的实现者能够为局部变量区设计任意的数据结构。好比对于怎样把longdouble类型的值存储到两个数组项中,Java虚拟机规范没有指定。假如某个虚拟机实现的字长为64位,这时就能够把整个longdouble数据放在数组中相邻两数组项的低项内,而使高项保持为空。

  操做数栈  和局部变量区同样,操做数栈也是被组织成一个以字长为单位的数组。可是和前者不一样的是,它不是经过索引来访问,而是经过标准的栈操做——压栈和出栈——来访问的。好比,若是某个指令把一个值压人到操做数栈中,稍后另外一个指令就能够弹出这个值来使用。

  虚拟机在操做数栈中存储数据的方式和在局部变量区中是同样的,如intlongfloatdoublereferencereturnType的存储。对于byteshort以及char类型的值在压入到操做数栈以前,也会被转换为int

  不一样于程序计数器,Java虚拟机没有寄存器,程序计数器也没法被程序指令直接访问。Java虚拟机的指令是从操做数栈中而不是从寄存器中取得操做数的,所以它的运行方式是基于栈的而不是基于寄存器的。虽然指令也能够从其余地方取得操做数,好比从字节流中跟随在操做码(表明指令的字节)以后的字节中或从常量池中,可是主要仍是从操做数栈中得到操做数。

  虚拟机把操做数栈做为它的工做区——大多数指令都要从这里弹出数据,执行运算,而后把结果压回操做数栈。好比,iadd指令就要从操做数栈中弹出两个整数,执行加法运算,其结果又压回到操做数栈中。看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:

iload_0  //push the int in local variable 0

iload_l  // push the int in local variable 1

iadd // pop two ints, add them, push result

istore_2 /7 pop int, store into local variable 2

  在这个字节码序列里,前两个指令iload_0iload_l将存储在局部变量区中索引为01的整数压人操做数栈中,其后iadd指令从操做数栈中弹出那两个整数相加,再将结果压入操做数栈。第四条指令istore_2则从操做数栈中弹出结果,并把它存储到局部变量区索引为2的位置。图5-10详细表述了这个过程当中局部变量和操做数栈的状态变化,阁中没有使用的局部变量区和操做数栈区域以空白表示。

  帧数据区  除了局部变量区和操做数栈外,Java栈帧还须要一些数据来支持常量池解析、正常方法返回以及异常派发机制,这些信息都保存在Java栈帧的帧数据区中。

  Java虚拟机中的大多数指令都涉及到常量池入口。有些指令仅仅是从常量池中取出数据而后压人Java栈(这些数据的类型包括intlongfloatdoubleString);还有些指令使用常暈池的数据来指示要实例化的类或数组、要访问的字段,或要调用的方法;还有些指令须要常量池中的数据才能肯定某个对象是否属于某个类或实现了某个接口。

  每一个虚拟机要执行某个须要到常量池数据的指令时,它都会经过帧数掘区中指向常量池的指针来访问它。之前讲过,常景池中对类型、字段和方法的引用在开始时都是符号。当虚拟机在常量池中搜索的时候,若是遇到指向类、接口、字段或者方法的入口,倘若它们仍然是符号,虚拟机那时候才会(也必须)进行解析。

  除了用于常量池的解析外,帧数据区还要帮助虚拟机处埋Java方法的正常结束或异常停止。若是是经过return正常结束,虚拟机必须恢复发起调用的方法的栈帧,包括设置PC寄存器指向发起调用的方法中的指令---即紧跟着调用了完成方法的指令的下一个指令。假如方法有返回值,虛拟机必须将它压入到发起调用的方法的操做数栈,为了处理Java方法执行期间的异常退出状况,帧数据区还必须保存一个对此方法异常表的引用。异常表会在第17章深刻描述,它定义了在这个方法的字节码中受catch子句保护的范围,异常表中的每一项都有一个被catch子句保护的代码的起始和结束位置(译者注:即try子句内部的代码),可能被catch的异常类在常量池中的索引值,以及catch子句内的代开始的位置。

  当某个方法抛出异常时,虚拟机根据帧数据区对应的异常表来决定如何处理。若是在异常表中找到了匹配的catch子句,就会把控制权转交给catch子句内的代码。若是没有发现,方法会当即异常终止。而后虚拟机使用帧数据区的信息恢复发起调用的方法的帧,而后在发起调用的方法的上下文中从新抛出一样的异常。

  除了上述信息(支持常量池解析、正常方法返回和异常派发的数据)外,虚拟机的实现者也能够将其余信息放人帧数据区,如用于调试的数据等。

本地方法栈

  前面提到的全部运行时数据区都是在Java虚拟机规范中明肯定义的,除此以外,对于一个运行中的Java程序而言,它还可能会用到一些跟本地方法相关的数据区。当某个线程调用一个本地方法时,它就进入了一个全新的而且再也不受虚拟机限制的世界。本地方法能够经过本地方法接口来访问虚拟机的运行时数据区,但不止于此,它还能够作任何它想作的事情。好比,它甚至能够直接使用本地处理器中的寄存器,或者直接从本地内存的堆中分配任意数量的内存等等。总之,它和虚拟机拥有一样的权限(或者说能力)

  本地方法本质上是依赖于实现的,虚拟机实现的设计者们能够自由地决定使用怎样的机制来让Java程序调用本地方法。

  任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会建立一个新的栈帧并压人Java栈。然而当它调用的是本地方法时,虛拟机会保持Java栈不变,再也不在线程的Java栈中压人新的帧,虚拟机只是简单地动态链接并直接调用指定的本地方法。能够把这看作是虚拟机利用本地方法来动态扩展本身。就如同lava虛拟机的实如今按照其中运行的Java程序的吩附,调用属于虚拟机内部的另外一个(动态链接的)方法。

  若是某个虚拟机实现的本地方法接口是使用C链接模型的话,那么它的本地方法栈就是C栈。咱们知道,当C程序调用一个C函数时,其栈操做都是肯定的。传递给该函数的参数以某个肯定的顺序压入栈,它的返回值也以肯定的方式传回调用者。一样,这就是该虚拟机实现中本地方法栈的行为。

  极可能本地方法接口须要回调Java虚拟机中的Java方法(这也是由设计者决定的),在这种情形下,该线程会保存本地方法栈的状态并进人到另外一个Java栈。

  图5-13描绘了这种状况,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另外一个Java方法。这幅图展现了Java虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操做它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。

  如图5-13所示,该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样致使虚拟机使用了一个本地方法栈。图中的本地方法栈显示为一个连续的内存空间。假设这是一个C语言栈,其间有两个C函数,它们都以包围在虚线中的灰色块表示。第一个C函数被第二个Java方法当作本地方法调用,而这个C函数又调用了第二个C函数。以后第二个C函数又经过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)

  就像其余运行时内存区同样,本地方法栈占用的内存区也没必要是固定大小的,它能够根据须要动态扩展或者收缩。某些实现也容许用户或者程序员指定该内存区的初始大小以及最大、最小值。

相关文章
相关标签/搜索