Java虚拟机方法区

Java虚拟机的加载子系统在加载一个类型(类或接口)的时候,主要完成以下三件事:

  • 由一个类型的全限定名查找对应的二进制流(可能class文件,也可能是数据库中的二进制或来自网络的字节流)
  • 根据二进制流转为虚拟机方法区中的运行时数据结构
  • 在Java堆中生成代表该类型的java.lang.Class对象,作为方法区类型数据的访问入口。
接下来就详细说说方法区中的运行时数据结构具体包括哪一些。在虚拟机规范中,对于方法区的规定十分抽象,因此跟具体的实现有很大的关系。但是一般都具有一些共同的部分。由于虚拟机的方法区使线程共享的,因此访问这些数据必须是线程安全的。根据《深入Java虚拟机》(Bill Venners)的描述,方法区中的数据包括以下内容:

类型信息(Type information)

  • 类型的全限定名(class文件中的this_class)
  • 类型的直接超类的全限定名(class文件中的super_class),Object类型除外。
  • 直接超接口的全限定名列表(对应interfaces表集合)
  • 该类型是类类型还是接口类型。
  • 类型的访问修饰符(如public、abstract、final等,对应access_flags)
其中的全限定名把Java源码中的“ . ”换成“ /  ”

类型常量池

常量池中存放的是该类型所用到的常量的有序集合,包括直接常量(CONSTANT_String、CONSTANT_Integer等)和对其他类型、字段和方法的符号引用(symbolic reference),这个常量池像数组一样通过索引来访问,在动态连接中起着核心作用。如果用javap  -verbose工具,可以得到虚拟机指令的“汇编程序”,可以看到指令中会用#后跟索引来表示对常量池的访问。例如getstatic #9。

字段信息

注意区别常量池中的字段引用与这里的字段信息。字段信息,是指该类中声明的所有字段(包括 类级变量和实例变量,不包括局部变量)的描述,如字段名称、字段修饰符等。
具体如下:
  • 字段名
  • 字段的类型(可能是基本类型或引用类型)
  • 字段的修饰符(public、static、transient等)
这些信息可以从class字节流中的fields_info中找到。注意的是,字段的顺序也需要保留,当然还可能包含其他一些信息。

方法信息

和字段很类似的,包括:
  • 方法名
  • 方法返回类型
  • 方法参数的个数和类型、顺序等
  • 方法的修饰符。
方法的顺序也是需要保存的,除此以外,如果方法不是抽象或本地的,还必须保存:
  • 方法的字节码(bytecodes)
  • 操作数栈和该方法在栈帧中的局部变量区的大小等。
  • 异常表
这些信息可以造class字节流的method_ref中找到。

类变量

由于类变量时所有对象共享的,因此并不保存在堆栈中,而是保存在方法区中。即使没有任何实例对象,也可以访问这些类变量。这些类变量只与类挂钩。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须为这些类变量分配内存空间(在连接过程的准备阶段,会为类变量分配内存并设置为默认值(而不是初值))。对于编译时常量,则直接将其复制到使用它们(编译时常量)的类的常量池当中,或者作为字节码流的一部分。 比如,类C声明了一个final static(即编译时常量)的字段field1和static的field2,在D类和E类中用到了field1,则在类D和类E的常量池中,都会保存有field1的副本。而field2则保存在类C的方法区当中。【英文原文: 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 thatuses them. 】

指向类加载器的引用:(A Reference to Class ClassLoader)

一个类可以被启动类加载器或者自定义的类加载器加载,如果一个类被某个自定义类加载器的对象(实例)加载,则方法区中必须保存对该对象的引用。例如,我们自己定义了一个类加载器GreeterClassLoader,并实例化一个实例myLoader,然后用myLoader加载类类C,则在C的方法区中,必须保存对myLoader的引用。这样,当类C引用了类D并且类D还未被加载时,虚拟机就会请求myLoader对象来加载类型D。

指向Class实例的引用

在加载过程中,虚拟机会创建一个代表该类型的Class对象,方法区中必须保存对该对象的引用。可以通过Class类的forName静态方法来得到该Class对象。例如Class.forName("java.lang.Thread")将会返回一个代表Thread类的Class对象。但是如果虚拟机无法将Class类加载到当前的命名空间,则会抛出ClassNotFoundException。另以后总方式可以得到Class对象的引用,例如我有一个Object的对象实例obj,则直接调用obj.getClass()就可以得到一个代表Object类的Class对象的引用。通过该引用,就可以获取一些类型信息,例如getName、getSuperClass等。

方法表

方法表是为了提高访问效率的,并不一定包含这一项。虚拟机可能会对每个装载的非抽象类,都声称一个方法表,作为一部分类型信息保存在方法区中。方法表示一个数组,每个数组元素是实例可能调用的方法的直接引用(指向方法信息),包括从超类继承来的方法。而对于抽象类和接口,保存方法表则毫无用处,因为不可能实例化一个抽象类或者接口。运行的时候就可以通过方法表快速搜寻对象调用的方法(常量池中的方法引用在解析的时候被解析成方法表索引??)。

区别字段引用和字段信息、方法引用与方法信息很重要。

下面是一个具体的实例,来说明方法区中的信息:

class Lava {
	private int speed = 5;

	void flow() {
		System.out.println("I am flowing 5 kilometers per hour");
	}
}

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

	}

}

首先必须告诉虚拟机初始类的名字是Volcano,例如你在命令行输入:java Volcano。虚拟机找到并读入想要的class文件Volcano.class。然从这个二进制流中提取类型信息并放到方法区中,经过一些诸如验证、准备等过程后,虚拟机开始执行main()方法,运行时,虚拟机持有指向当前类的常量池的指针。需要注意的是,开始运行main()方法的字节码时, Lava类还没有装载。虚拟机至于在需要的时候才装载相应的类。main方法虚拟机指令如下:



第一条“汇编指令” new  #16 告诉虚拟机为常量池的第16项分配好足够的内存,所以虚拟机通过指向常量池的指针找到第16项,发现它是一个对Lava类的符号引用,接着检查方法区,看Lava是否已经被装载。发现哈没有装载后,它就开始查找Lava.class的文件,并且读入到方法区中。读入之后,就将16号符号引用替换为直接引用(本地指针、地址),以后就可以通过这个指针快速访问Lava类。这个过程也被称为常量池解析。

现在,虚拟机准备为一个Lava对象分配内存了,通过第16项的直接引用,可以访问Lava类型信息,然后计算出一个Lava独享需要多少内存。OK,将new指令生成的lava对象引用压入栈顶。然后通过这个引用调用invokespecial #18,这是对象的初始化方法<init>,将speed赋值为5. astore_1指令将lava对象引用存入到局部变量表的1号slot。 aload_1将引用压入栈顶,然后执行 invokevirtual #19,即调用lava对象的flow方法。最后返回,结束main方法。

方法区使很重要的内存区域,需要在以后不断总结。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值