方法区
在java虚拟机中,方法区用于存储加载类型的信息。当虚拟机加载一个类型时,通过类加载器定位并读取符合条件的class文件(线性的二进制数据流),然后把从二进制数据中获取的类型信息存储到方法区中。类变量(static变量)的内存也来自方法区。
类型信息
对于每个加载的类型,java虚拟机需要将以下信息存储到方法区中:
- 类型的全限定名
- 类型直接父类的全限定名(除非类型时interface或者Object类)
- 该类型是属于class还是interface
- 类型的修饰符(public,abstract, final的子集)
- 一个包含所有直接接口全限定名的有序列表
在class文件和java虚拟机内部,类型的名字总是以全限定名的方式存储。在java源代码中,全限定名由类型的包名,加一个点,再加上类型的名字组成。例如,包java.lang中的Object类的全限定名是java.lang.Object。在class文件中,点被斜杠取代,如java/lang/Object。
除了上面所列的基本类型信息之外,虚拟机还需要为加载的类型存储下面的内容:
常量池是类型使用的一个有序常量集,包括字面值(字符串,整数,浮点数常量)和对类型,域和方法的符号引用。常量池的条目通过索引来引用,跟引用数组元素很像。因为它持有一个类型用到的所有类型,域和方法的符号引用,所以常量池在java程序的动态链接中起着重要的作用。
域信息
对于类型中声明的每一个域,必须把下面的信息存储到方法区里。除了每个域的信息之外,域在类型中声明的顺序也会被记录下来:
- 域名
- 域的类型
- 域的修饰符(public,private,protected,static,final,volatile,transient的子集)
方法信息
对于类型中声明的每一个方法,下面的信息必须存储到方法区中。跟域一样,方法在类型中声明的顺序会被记录起来:
- 方法名
- 方法返回值(后者void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的子集)
对于非abstract或者native的方法,还需要存储以下信息:
- 方法的字节码
- 操作数栈的大小和方法栈帧中的本地变量部分
- 异常表
类变量
类变量是被类的所有实例共享的,并且可以不需要通过实例来访问。这些变量与类关联,不与类的实例关联,所以它们在逻辑上属于方法区类数据的一部分。在java虚拟机使用一个类之前,它必须为每个非final类变量从方法区中分配内存。
常量(声明为final的类变量)的处理方式与非final的类变量不同,每个使用final类变量的类型会从它自身的常量池中获取一个常量值的拷贝。作为常量池的一部分,final类变量存储在方法区。
一个ClassLoader类的引用
java虚拟机对于每个加载的类型,都会记录这个类型是通过bootstrap类加载器还是通过user-defined类加载器被加载。对于通过usero-defined类加载器加载的这些类型,虚拟机需要存储这个类加载器的引用,这个信息作为类型数据的部分存储在方法区里。虚拟机在进行动态链接期间使用这个信息,当一个类型引用另一个类型时,虚拟机从同一个类加载器(引用类型的类加载器)中请求被引用的类型,这个动态链接的过程也是虚拟机形成各自独立的名字空间的关键途径。
一个Class类的引用
java虚拟机会为其加载的每个类型创建一个java.lang.Class类的实例。虚拟机必须以某种方式将一个类型的Class实例引用与方法区中的类型数据关联起来。
在java程序中可以获取并使用Class对象的引用。一种方法是调用Class类中有个静态方法:
- // A method declared in class java.lang.Class:
- public static Class forName(String className);
另一种方法是调用任何对象引用的getClass()方法:
- // A method declared in class java.lang.Object:
- public final Class getClass();
- // 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();
方法表
java虚拟机加载的每非抽象类,它都会生成一个方法表,并作为方法区的类信息的一部分存储起来。方法表是对所有实例方法的直接引用的数组,包括从基类中继承实例方法。方法表使得虚拟机可以快速定位到对象调用的实例方法。
方法区使用的例子
下面的例子将介绍虚拟机如何使用存储在方法区里的信息。
- 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();
- }
- }
注意java虚拟机已经开始执行Volcano类main()方法的字节码,尽管它还没有把Lava类加载进来。很多java虚拟机实现都是在需要的时候才把特定的类加载进来。
main()方法中的第一条指令告诉java虚拟机为常量池的第一个条目中所列的类分配足够的内存空间。java虚拟机使用它的指向Volcano常量池的指针来查找条目一并找到一个对Lava类的符号引用,符号引用只是一个给定类的全限定名字符串。java虚拟机会通过检查方法区看Lava类是否已经被加载了。当java虚拟机发现它还没有加载名字为"Lava"的类时,它就开始查找并读取Lava.class文件,它从导入的二进制数据中获取Lava类的定义,并将这些信息存储到方法区。
然后java虚拟机将Volcano常量池条目一中的符号引用(Lava字符串)替换成一个对Lava的类数据的指针。如果java虚拟机后面需要再次使用Volcano常量池的条目一,就不需要再进行这个相对较慢的根据符号引用查找并加载数据的过程,它可以直接使用指针快速地访问Lava的类数据。这个将符号引用替换成直接引用(一个本地指针)的过程称为常量池解析。
最后,虚拟机就准备真正地为一个新的Lava对象分配内存空间。虚拟机会再一次查询存储在方法区中的信息,使用指向Lava数据的指针(Volcano常量池的条目一)计算出一个Lava对象需要多大的堆空间(一ava虚拟机总是可以通过查找存储在方法区中的类数据来决定表示一个对象需要多大的内存空间)。
main()方法的第一条指令完成后会将新的Lava对象的引用push到栈中。后面的指令会使用这个引用来调用java代码对speed变量初始化成正确的初始值5,下一条指令会使用这个引用来调用Lava对象的flow()方法。