JVM中的方法区(Method Area)(转载/整理)
Java在内存中专门划分出一块静态存储区域(即在固定位置上存放应用程序运行时一直存在的数据,由于位置固定,下次调用时便省去了查找的麻烦),称为方法区。
类型信息(这里的类型指类或接口)是由类加载器在类加载时从类文件中提取出来的,被存储在方法区。另外,类(静态)变量也存储在方法区中。JVM在运行应用时要大量使用存储在方法区中的类型信息。在类型信息的表示上,设计者除了要尽可能提高应用的运行效率外,还要考虑空间问题。根据不同的需求,JVM的实现者可以在时间和空间上追求一种平衡。
对每个加载的类型,jvm必须在方法区中存储以下类型信息:
1)这个类型的完整有效名(全限定名)
2)这个类型直接父类的完整有效名(除非这个类型是interface或是java.lang.Object,两种情况下都没有父类)
3)这个类型的修饰符(public,abstract, final的某个子集)
4)这个类型直接超接口的一个有序列表(全限定名列表)
注意:类型名称在java类文件和jvm中都以完整有效名出现。在java源代码中,完整有效名由类的所属包名称加一个".",再加上类名组成。例如,类Object的所属包为java.lang,那它的完整名称为java.lang.Object,但在类文件里,所有的"."都被斜杠“/”代替,就成为java/lang/Object。完整有效名在方法区中的表示根据不同的实现而不同。
除了以上的基本信息外,jvm还要为每个类型保存以下信息:
1)类型的常量池( constant pool)
2)域(Field)信息
3)方法(Method)信息
4)除了常量外的所有静态(static)变量
常量池
JVM为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string,integer, 和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。因为常量池存储了一个类型所使用到的所有类型,域和方法的符号引用,所以它在java程序的动态链接中起了核心的作用。
Constant Pool常量池的概念:
在讲到String的一些特殊情况时,总会提到String Pool或者Constant Pool,但是我想很多人都不太白Constant Pool到底是个怎么样的东西,运行的时候存储在哪里,所以在这里先说一下Constant Pool的内容。
String Pool是对应于在Constant Pool中存储String常量的区域.习惯称为String Pool,也有人称为String Constant Pool。
在java编译好的class文件中,有个区域称为Constant Pool,他是一个由数组组成的表,类型为cp_info constant_pool[],用来存储程序中使用的各种常量,包括Class/String/Integer等各种基本Java数据类型
对于Constant Pool,表的基本通用结构为:
cp_info {
u1 tag;
u1 info[];
}
tag是一个数字,用来表示存储的常量的类型,例如8表示String类型,5表示Long类型,info[]根据类型码tag的不同会发生相应变化。
例如:对于String类型,表的结构为:
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
域信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序,域的相关信息包括:
1)域名
2)域类型
3)域修饰符(public、private、protected、static、final、volatile、transient的某个子集)
方法信息
JVM必须保存所有方法的以下信息,同样域信息一样包括声明顺序
方法名
方法的返回类型(或void)
方法参数的数量和类型(有序的)
方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)除了abstract和native方法外,其他方法还有保存方法的字节码(bytecodes)操作数栈和方法栈帧(栈以帧为单位保存线程的状态)的局部变量区的大小
异常表
类变量(Class Variables,就是类的静态变量,它只与类相关,所以称为类变量)
类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在JVM使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。被声明为final的类变量的处理方法则不同,每个常量都会在常量池中有一个拷贝。non-final类变量被存储在声明它的类信息内,而final类变量被存储在所有使用它的类信息内。
对类加载器的引用
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。
JVM在动态链接的时候需要这个信息。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。这对JVM区分名字空间的方式是至关重要的。
对Class类的引用
JVM为每个加载的类型(译者:包括类和接口)都创建一个java.lang.Class的实例。而JVM必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。
方法表
为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,JVM的实现者还可以添加一些其他的数据结构,如方法表。JVM对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法)。JVM可以通过方法表快速激活实例方法。(译者:这里的方法表与C++中的虚拟函数表一样,但Java方法全都是virtual的,自然也不用虚拟二字了。正像Java宣称没有指针了,其实Java里全是指针。更安全只是加了更完备的检查机制,但这都是以牺牲效率为代价的,个人认为Java的设计者始终是把安全放在效率之上的,所有Java才更适合于网络开发)
我们以下面一个例子,看JVM如何使用方法区中的信息。
看下面这个类:
class Lava {
private int speed = 5; // 5 kilometers per hour
void flow() {
}
}
class Volcano {
public static void main(String[] args) {
Lava lava = new Lava();
lava.flow();
}
}
下面我们描述一下main()方法的第一条指令的字节码是如何被执行的。不同的JVM实现的差别很大,这里只是可能的其中之一的情形。
为了运行这个程序,你以某种方式把“Volcano”传给了JVM。有了这个名字,JVM找到了这个类文件(Volcano.class)并读入,它从类文件提取了类型信息并放在了方法区中,通过解析存在方法区中的字节码,JVM激活了main()方法,在执行时,JVM保持了一个指向当前类(Volcano)常量池的指针。
注意JVM在还没有加载Lava类的时候就已经开始执行了。正像大多数的JVM一样,不会等所有类都加载了以后才开始执行,它只会在需要的时候才加载。
main()的第一条指令告知JVM为常量池第一项的类分配足够的内存。JVM使用指向Volcano常量池的指针找到第一项,发现是一个对Lava类的符号引用,然后它就检查方法区看Lava是否已经被加载了。
这个符号引用仅仅是类Lava的完整有效名“Lava”。这里我们看到为了JVM能尽快从一个名称找到一个类,一个良好的数据结构是多么重要。这里JVM的实现者可以采用各种方法,如hash表,查找树等等。同样的算法可以用于Class类的forName()的实现。
当JVM发现还没有加载过一个称为"Lava"的类,它就开始查找并加载类文件"Lava.class"。它从类文件中抽取类型信息并放在了方法区中。
JVM于是以一个直接指向方法区Lava类的指针替换了常量池第一项的符号引用。以后就可以用这个指针快速的找到Lava类了。而这个替换过程称为常量池解析(constant pool resolution)。在这里我们替换的是一个native指针。
JVM终于开始为新的Lava对象分配空间了。这次,JVM仍然需要方法区中的信息。它使用指向Lava数据的指针(刚才指向Volcano常量池第一项的指针)找到一个Lava对象究竟需要多少空间。
JVM总能够从存储在方法区中的类型信息知道某类型对象需要的空间。但一个对象在不同的JVM中可能需要不同的空间,而且它的空间分布也是不同的。(译者:这与在C++中,不同的编译器也有不同的对象模型是一个道理)
一旦JVM知道了一个Lava对象所要的空间,它就在堆上分配这个空间并把这个实例的变量speed初始化为缺省值0。假如lava的父对象也有实例变量,则也会初始化。
当把新生成的Lava对象的引用压入栈中,第一条指令也结束了。