本文按
七 方法区(method area)
八 堆(Heap)
九 程序计数器(The Program Counter)
十 Java 栈
七 方法区(method area)
当虚拟机需要加载一个Type的时候,它使用classloader来定位具体的类位置。Classloader读取class file并且把字节码传递到虚拟机中。虚拟机提取字节码中的类型信息,存储到method area。
方法区的特点:
(1)所有的线程共享方法区,所以访问方法区上的数据结构必须是线程安全的,如果两个线程同时试图找到Lava这个类,并且Lava还没有加载,那么只有一个线程可以去加载这个类,其它线程需要等待。
(2)方法区的大小不必固定,随着程序的运行,虚拟机会根据应用的需求自动伸缩方法区。同时方法区也可以被垃圾收集,如果一个class不被引用了,虚拟机可以回收这个class并确保方法区最小。
7.1 类型信息(Type Information)
虚拟机对于其加载的任何类型,都会存储下面的信息到方法区:
(1) 类型的全限定名
(2) 当前类型最直接的父类
(3) 类型是class 还是 interface
(4) 类型修饰信息(比如public abstract final)
(5) 父接口的list
除了上面必须存储的基本信息外,虚拟机还需要为每个Type存存储下面的信息:
(1) 类型的常量池(constant pool)信息
(2) Field 信息
(3) Method 信息
(4) 所有的类变量(static veriables)信息, 声明为constant的除外
(5) 指向classLoader的引用
(6) 指向Class的引用
7.1.1 常量池(constant pool)
对于任何一个被虚拟机加载的类型,必须为这个类型存储一个常量池。常量池是有序的一组常量(被final修饰的静态变量、实例变量和局部变量),包括字面量(string, integer, and floating指向的常量)和针对类型,域和方法的符号引用。可以像访问数组那样通过下标进入到常量池。正因为常量池中存储了所有针对类型(Type),域(Field)和方法(Method)的符号引用,常量池在java动态链接程序中非常重要。
7.1.2 域信息(Field Information)
对于每个在类型中定义的域,method area中需要存储以下的信息:
(1) Field name
(2) Field type
(3) Field 修饰符(public, private, protected, static, final, volatile, transient)
7.1.3 方法信息
对于每一个在类型中定义的方法,method area中需要存储下面的信息:
(1) 方法名
(2) 返回类型
(3) 参数个数和类型
(4) 修饰符(public, private, protected, static, final, synchronized, native, abstract)
此外,对于非abstrat 和 非native的方法还要存储下面的信息:
(1) 方法的二进制数据
(2) 操作符栈和本地变量区域
(3) 异常表
7.1.4 类变量
类变量对所有实例共享而且独立于类存在,所以,在虚拟机使用一个class 之前,它必须为方法区中每一个不是final的类变量开辟内存。
常量-被声明为final的变量,和non-final的变量区别对待。每一个Type在自己的线程池中会得到一个常量的副本。常量也会在method area中存储。
7.1.5 classoader的引用
对于每一个加载的Type,虚拟机需要知道它是被bootstrap classloader 加载的还是被user-defined class loader加载的。
这些信息对于动态链接程序非常重要。
7.1.6 Class 类的引用
java.lang.Class类为我们提供了获取class 信息的方式,每一个它的实例都是虚拟机为它加载的Type创建的。它包含下面的方法:
public String getName();
public Class getSuperClass();
public boolean isInterface();
public Class[] getInterfaces();
public ClassLoader getClassLoader();
八 堆(Heap)
我们已经知道堆中主要存储的是对象的实例,虚拟机规范中并没有规定这些实例如何在堆中呈现。因而对于一个对象引用,如果虚拟机需要快速定位到实例,就需要在独享的内存被开辟的时候同时存储一个某种 类型的指针指向方法区。
8.1 访问堆中对象的方式:
8.1.1 双指针
一种可能的堆设计是将堆分成两部分:操作池(handle pool)和对象池(object pool)。对象的引用是一个指向handle pool入口的native的指针。Handle pool的入口也有两部分组成:一个指向对象池实例的指针和一个method area中class data的指针。这样设计的好处是:当虚拟机需要将一个对象挪到object pool中的时候,它只需要将一个指针指向新的地址就可以了。缺点是每一次访问对象实例都需要遵从两个指针。这种模式如下图所示:
8.1.2 使用本地指针访问对象
另一种方式是一个对象引用一个本地指针,指向一组对象中的实例,还包括一个指向classdata的指针。这种方式只需要遵循一个指针,但是移动对象变得困难。当虚拟机需要移动一个对象来清理残片的时候,它必须更新每一个对这个对象的引用。这种设计方法如下图所示:
很多时候我们需要根据类型信息访问对象信息,比如正在运行的程序试图将对象引用转换为类型(Type). 这时虚拟机需要检查将要转化的类型和实际的类型信息是否相符。同理在程序使用instance of的时候。在任何一种情况下,虚拟机必须去查看对象引用的类信息。
九 程序计数器(The Program Counter)
正在执行的每一个程序的线程都有一个自己的程序计数器(PC Register),它在线程创建的时候被创建。大小为1个字,所以它可以存放native pointer 和returnaddress。程序计数器中存储正在执行的指令的地址。
十 Java 栈
当一个新的线程启动的时候,虚拟机为新的线程创建一个新的Java 栈。Java栈帧中离散存储线程的状态。虚拟机对Java栈只执行两项操作:push & pop frame.
当线程调用一个方法的时候,虚拟机创建一个栈帧并把它push到当前线程的 Java 栈中去,这个新的栈帧随即变成当前帧,随着方法的执行,它用帧来存储参数,本地变量,中间计算结果和其它数据。
方法执行会以两种方式结束:成功返回或者抛出异常,无论方法以何种方式结束虚拟机都会pop出栈帧,病丢弃。
Java栈中的所有数据对于执行线程都是私有的,其它线程不.能访问或者修改当前线程栈帧中的数据。
10.1 Java栈帧(Stack Frame)
栈帧由三部分组成:本地变量(local variables),操作符栈(operand stack),帧数据(Frame Data)
10.1.1 本地变量
Java栈中的本地变量区域是一个从0开始的数组。int, float, reference, 和returnAddress占用一个条目, byte, short, 和char在他们进入数组之前会被转换成int。如下图所示:
class Example3a {
public static int runClassMethod(int i, long l, float f,
double d, Object o, byte b) {
return 0;
}
public int runInstanceMethod(char c, double d, short s,
boolean b) {
return 0;
}
}
注意:runInstanceMethod中的第一个数据十reference,但是实际上它并不存在在方法的参数中,这个被隐藏的变量被分配给所有实例的方法。
10.1.2 操作数栈
操作符栈和本地变量一样按照数组的形式组织,不同的是本地变量可以按照index取值,但是操作数栈使用push和pop操作。如果一个指令向操作数栈中push了一个值,后续的指令可以使用或者pop掉它。
数据类型的存储和本地变量一样。
不同于不能直接被指令访问的程序计数器, Java虚拟机没有寄存器。Java虚拟机是基于栈的而不是基于寄存器的,因为虚拟机从栈中获取操作数而不是从寄存器中获取操作数。
Java虚拟机从栈中获取操作数,运算,得到结果后回写到栈中。下面我们看虚拟机是如何执行加操作的
iload_0 // push the int in local variable 0
iload_1 // push the int in local variable 1
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
10.1.3 帧数据(Frame Data)
除了本地变量和操作数栈,Java栈帧中还包含了一些数据,用来支持常量池变换,方法返回和异常分发。这些数据存储在Frame Data中。