Java虚拟机体系结构
方法区
在Java虚拟机中,被装载类型的信息存储在一个逻辑上被称为方法区的内存中。
当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,-->读入这个class文件(一个线性的二进制流)->将它传入虚拟机-->
虚拟机提取类型信息,并将信息存入方法区,类型中的类(静态)变量也存储在方法区.
方法区特点:
1)所有线程共享方法区。它是线程安全的。
2)方法区大小不是固定的。虚拟机根据需要自行调整。
3)方法区可以被垃圾回收。
对于每个被装载的类型,虚拟机会在方法区中存储以下信息。
1)类型的基本信息;
a)类型的全限定名
b)类型的直接超类全限定名(除非这个类型是java.lang.Objet,它没超类)。
c)类型是类类型还是接口类型(就是说是一个类还是一个接口)。
d)类型的访问修饰符(public ,abstract或final的某个子类)
e)任何直接超接口的全限定名的有序列表。
2)该类型的常量池
虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,
包括直接常量(string,integer,floating point常量)和对其他类型、字段和方法的符号引用。
池中的数据项就像数组一样是通过索引访问的。因为常量池存储了相应类型所用到的所有类型、
字段和方法的符号引用,所以它在Java程序的动态连接中起着核心的作用。
3)字段信息
类型中声明的每一个字段,方法区中必须保存下面的信息,字段在类或接口中声明的顺序也必须保存。
字段名,字段类型,字段修饰符(public private protected static final 等)
4)方法信息
类型中声明的每一个方法,方法区中必须保存下面的信息,方法在类或接口中声明的顺序也必须保存。
方法名,返回值类型,参数数量和类型(按声明顺序),方法修饰符(public private protected static final 等)
如果方法不是抽象的或本地的还必须保存:方法字节码,操作数栈和该方法在栈针中局部变量的大小,异常表。
5)除了常量以外的所有类(静态)变量
这里主要说下编译时常量:就是那些用final声明以及编译时已知的值初始化的类变量(例如:static final int val =5)
每个编译时常量的类型都会复制它所有常量到它自己的常量池中或者它的字节码流中(通常情况下编译时直接替换字节码)。
6)一个到类classLoader的引用
指向ClassLoader类的引用 每个类型被装载的时候,虚拟机必须跟踪它是由启动类装载器
还是由用户自定义类装载器装载的。如果是用户自定义类装载器装载的,那么虚拟机必须在类
型信息中存储对该装载器的引用:这是作为方法表中的类型数据的一部分保存的。
虚拟机会在动态连按期间使用这个信息。当某个类型引用另一个类型的时候,虚拟机会请求装载
发起引用类型的类装载器来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间
的方式也是至关重要的。为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方
法表中得知每个类都是由哪个类装载器装载的。
7)一个到Class类的引用
指向Class类的引用 对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为
它创建一个java.lang.Class类的实例(Class实例放在内存中的堆区),而且虚拟机还必须以某种方式把这个实例的引用存储在方法区
为了尽可能提高访问效率,设计者必须仔细设计存储在方法区中的类型信息的数据结构,因此,
除了以上时论的原始类型信息,实现中还可能包括其他数据结构以加快访问原始数据的速度,比如方法表。
虚拟机对每个装载的非抽象类,都生成一个方法表,把它作为类信息的一部分保存在方法区。方法表是一个数组,
它的元素是所有它的实例可能被调用的实例方法的直接引用,包括那些从超类继承过来的实例方法:(对于抽象类和接口,方法表没有什么帮
助,因为程序决不会生成它们的实例。)运行时可以通过方法表快速搜寻在对象中调用的实例方法。
方法区使用的例子
class Lava{
private int speed = 5;
void flow(){
}
}
public class Volcano {
public static void main(String args[]){
Lava lava = new Lava();
lava.flow();
}
}
1)虚拟机在方法区查找Volcano这个名字,未果,载入volcano.class文件,并提取相应信息
存入方法区。
2)虚拟机开始执行Volcano类中main()方法的字节码的时候,尽管Lava类还没被装载,
但是和大多数(也许所有)虚拟机实现一样,它不会等到把程序中用到的所有类都装载后才开
始运行程序。恰好相反,它只在需要时才装载相应的类。
3)main()的第一条指令告知虚拟机为列在常量池第一项的类分配足够的内存。所以虚拟机
使用指向Volcano常量池的指针找到第一项,发现它是一个对Lava类的符号引用,然后它就检查
方法区,看Lava类是否已经被装载了。
4)当虚拟机发现还没有装载过名为"Lava"的类时,它就开始查找并装载文件“Lava.class”,
并把从读入的二进制数据中提取的类型信息放在方法区中。
5)虚拟机以一个直接指向方法区Lava类数据的指针来替换常量池第—项(就是那个
字符串“Lava”)——以后就可以用这个指针来快速地访问Lava类了。这个替换过程称为常量池
解析,即把常量池中的符号引用替换为直接引用:这是通过在方法区中搜索被引用的元素实现
的,在这期间可能又需要装载其他类。在这里,我们替换掉符号引用的“直接引用”是一个本
地指针。
6)虚拟机准备为一个新的Lava对象分配内存。此时,它又需要方法区中的信息。还记
得刚刚放到Volcano类常量池第——项的指针吗?现在虚拟机用它来访问Lava类型信息(此前刚放
到方法区中的),找出其中记录的这样一个信息:一个Lava对象需要分配多少堆空间。
7)虚拟机确定一个Lava对象大小后,就在堆上分配空间,并把这个对象实例变量speed初始化为默认初始值0
8)当把新生成的Lava对象的引用压到栈中,main()方法的第一条指令也完成了,指令通过这个引用
调用Java代码(该代码把speed变量初始化为正确初始值5).另外用这个引用调用Lava对象引用的flow()方法。
堆
每个java虚拟机实例都有一个方法区以及一个堆,一个java程序独占一个java虚拟机实例,而每个java程序都有自己的堆空间,它们不会彼此干扰,但同一个java程序的多个线程共享一个堆空间。这种情况下要考虑多线程访问同步问题。
Java栈
一个新线程被创建时,都会得到自己的PC寄存器和一个java栈,虚拟机为每个线程开辟内存区。这些内存区是私有的,任何线程不能访问其他线程的PC寄存器和java栈。java栈总是存储该线程中java方法的调用状态。包括它的局部变量,被调用时传进来的参数,它的返回值,以及运算的中间结果等。java栈是由许多栈帧或者说帧组成,一个栈帧包含一个java方法的调用状态,当线程调用java方法时,虚拟机压入一个新的栈帧到该线程的java栈中。当方法返回时,这个栈帧被从java栈中弹出并抛弃。
.本地方法栈
任何本地方法接口都会使用某种本地方法饯。当线程调用Java方法时,虚拟机会创建一个新的栈帧井压人Java栈。
然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压人新的帧,虚拟机只是简单地动态连接
并直接调用指定的本地方法。可以把这看做是虚拟机利用本地方法来动态扩展自己。