深入Java虚拟机第5章 Java虚拟机 方法区

193 篇文章 9 订阅
107 篇文章 0 订阅

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

Java虚拟机在内部如何存储类型信息,这是由具体实现的设计者来决定的。比如,在class文件中,多字节值总是以高位在前(即代表较大数的字节在前)的顺序存储。但是当这些数据被引人到方法区后,虚拟机可以以任何方式存储它。假设某个实现是运行在低位优先的处理器上, 那么它很可能会把多字节值以低位优先的顺序存储到方法区。

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

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

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

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

类型信息(Type Information)

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

  • 这个类型的全限定名
  • 这个类型的直接超类的全限定名(除非这个类型是java.lang.Object,它没有超类)
  • 这个类型是类类型还是接口类型
  • 这个类型的访问修饰符(public、abstract或final的某个子集)
  • 任何直接超接口的全限定名的有序列表

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

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

  • 该类型的常量池。
  • 字段信息。
  • 方法信息。
  • 除了常量以外的所有类(静态)变量。
  • —个到类ClassLoader的引用。
  • 一个到Class类的引用。

在下面的小节中会描述这些数据。

常量池(The Constant Pool)

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

字段信息(Field Information)

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

  • 字段名
  • 字段的类型
  • 字段的修饰符(public、private、protected、static、final、volatile、transient的某个子集)

方法信息(Method Information)

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

  • 方法名
  • 方法的返回类型(或void)
  • 方法参数的数量和类型(按声明顺序)
  • 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)

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

  • 方法的字节码(bytecodes)
  • 操作数栈和该方法的栈帧中的局部变量区的大小
  • 异常表

类(静态)变量(Class Variables)

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

而编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量)则和一般的类变量的处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。作为常量池或字节码流的一部分,编译时常量保存在方法区中——就和一般的类变量一样。但是一般的类变量作为声明它们的类型的一部分数据而保存,而编译时常量作为使用它们的类型的一部分数据而保存。 这种特殊处理方式在第6章中更详细地讨论。

Class variables are shared among all instances of a class and can be accessed even in the absence of any instance. These variables are associated with the class–not with instances of the class–so they are logically part of the class data in the method area. Before a Java virtual machine uses a class, it must allocate memory from the method area for each non-final class variable declared in the class.

Constants (class variables declared final) are not treated in the same way as non-final class variables. Every type that uses a final class variable gets a copy of the constant value in its own constant pool. As part of the constant pool, final class variables are stored in the method area–just like non-final class variables. But whereas non-final class variables are stored as part of the data for the type that declares them, final class variables are stored as part of the data for any type that uses them. This special treatment of constants is explained in more detail in Chapter 6, “The Java Class File.”

指向ClassLoader类的引用(A Reference to Class ClassLoader)

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

虚拟机会在动态连接期间使用这个信息。当某个类型引用另一个类型的时候,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的方式也是至关重要的。为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的。 关于动态连接和命名空间的细节请 参见第8章。

指向Class类的引用(A Reference to Class Class)

对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来

在你的Java程序中,你可以得到并使用指向Class对象的引用。Class类中的一个静态方法可以让用户得到任何已装载的类的Class实例的引用。

// A method declared in class java.lang.Class:
public static Class forName(String className);

比如,如果调用forName(“java.lang.Object”),那么将得到一个代表java.lang.Object的Class对象的引用。可以使用forName()来得到代表任何包中任何类型的Class对象的引用,只要这个类型可以被(或者已经被)装载到当前命名空间中。如果虚拟机无法把请求的类型装载到当前命名空间,那么 forName() 会抛出ClassNotFoundException异常。

另一个得到Class对象引用的方法是,可以调用任何对象引用的getClass()方法。这个方法被来自Object类本身的所有对象继承:

// A method declared in class java.lang.Object:
public final Class getClass();

比如,如果你有一个到java.lang.Integer类的对象的引用,那么你只需简单地调用Integer对象引用的getClass()方法,就可以得到表示java.lang.Integer类的Class对象。

给出一个指向Class对象的引用,就可以通过Class类中定义的方法来找出这个类型的相关信息。如果查看这些方法,会很快意识到,Class类使得运行程序可以访问方法区中保存的信息。下面是Class类中声明的方法:

// Some of the methods declared in class java.lang.Class:
public String getName();
public Class getSuperClass();
public boolean isInterface();
public Class[] getInterfaces();
public ClassLoader getClassLoader();

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

方法表(Method Tables)

为了尽可能提高访问效率,设计者必须仔细设计存储在方法区中的类型信息的数据结构,因此,除了以上讨论的原始类型信息,实现中还可能包括其他数据结构以加快访问原始数据的速度,比如方法表。虚拟机对每个装载的非抽象类,都生成一个方法表,把它作为类信息(class information)的一部分保存在方法区。方法表是一个数组,它的元素是所有它的实例可能被调用的实例方法的直接引用,包括那些从超类继承过来的实例方法。(对于抽象类和接口,方法表没有什么帮助,因为程序绝不会生成它们的实例。)运行时可以通过方法表快速搜索在对象中调用的实例方法。方法表在第8章将深入探讨。

方法区使用示例(An Example of Method Area Use)

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

// On CD-ROM in file jvm/ex2/Lava.java
class Lava {

    private int speed = 5; // 5 kilometers per hour

    void flow() {
    }
}

// On CD-ROM in file jvm/ex2/Volcano.java
class Volcano {

    public static void main(String[] args) {
        Lava lava = new Lava();
        lava.flow();
    }
}

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

要运行Volcano程序,首先得以某种“依赖于实现的”方式告诉虚拟机“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也有实例变量,则也会在此时被初始化为相应的默认值。更多信息请参考第7章。

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

下面是Lava.java和Volcano.java的字节码:


// class version 51.0 (51)
// access flags 0x20
class jvm/ex2/Lava {

  // compiled from: Lava.java

  // access flags 0x2
  private I speed

  // access flags 0x0
  <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 5 L1
    ALOAD 0
    ICONST_5
    PUTFIELD jvm/ex2/Lava.speed : I
    RETURN
   L2
    LOCALVARIABLE this Ljvm/ex2/Lava; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x0
  flow()V
   L0
    LINENUMBER 8 L0
    RETURN
   L1
    LOCALVARIABLE this Ljvm/ex2/Lava; L0 L1 0
    MAXSTACK = 0
    MAXLOCALS = 1
}




// class version 51.0 (51)
// access flags 0x20
class jvm/ex2/Volcano {

  // compiled from: Volcano.java

  // access flags 0x0
  <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Ljvm/ex2/Volcano; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 8 L0
    NEW jvm/ex2/Lava
    DUP
    INVOKESPECIAL jvm/ex2/Lava.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 9 L1
    ALOAD 1
    INVOKEVIRTUAL jvm/ex2/Lava.flow ()V
   L2
    LINENUMBER 10 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE lava Ljvm/ex2/Lava; L1 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2
}


参考:
深入Java虚拟机(原书第2版) 第5章 Java虚拟机

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值