Java程序中初始的main()方法,作为该程序初始线程的起点。任何其它的线程,都是有这个初始线程启动的。
在JAVA虚拟机内部,有两种类型的线程:守护线程和非守护线程(实时线程)。比如执行垃圾收集任务的线程,就是一种守护线程。我们也可以把我们自己创建的线程标记为守护线程。初始线程,不是守护线程。
当虚拟机中所有的实时线程都结束的时候,虚拟机将自动退出(当然,也可以调用Runtime或System类中exit()方法来退出虚拟机)
如果main()方法执行完毕返回,而且在其中并没有启动其它的实时线程,那么唯一的一个实时线程(即初始线程)结束,这样,虚拟机就自动退出了。
<本图片来源于:http://www.artima.com/insidejvm/ed2/jvm2.html >
Java虚拟机运行一个程序,需要内存来存储许多东西,例如:字节码、从已转载的class文件中得到的其它信息、程序创建的对象、传递给方法的参数、返回值、局部变量以及运算的中间结果等等。Java虚拟机把这些数据都放到运行时数据区中。
方法区和堆中的数据,是所有线程共享的区域。方法区存储的是已经加载的类的信息。在程序运行过程中所创建的所有对象,均放到堆中。
当每一个新线程被创建时,它都会拥有自己的PC寄存器(pc register)(程序计数器,PC是program counter的缩写)以及一个JAVA栈。如果线程正在执行的是一个JAVA方法(非本地方法),PC寄存器中的值将总是指示下一条被执行的指令,而它的JAVA栈中存储的就是这个方法调用的状态:包括局部变量、方法被调用时传进来的参数、运算的中间结果以及返回值。而本地方法(native方法,与JNI有关)调用的状态,则存储与本地方法栈或别的地方(这与具体实现有关)。
Java栈由栈帧组成,每个栈帧包含一个JAVA方法调用的状态。当线程调用一个JAVA方法时,虚拟机压入新的栈帧到该线程的栈中,当该方法返回时,此栈帧被弹出并被抛弃。
上图描述了三个线程正在执行,线程1和2在执行JAVA方法,而线程3正在执行本地方法。
数据类型分两种:基本类型和引用类型,基本类型的变量持有原始值,而引用类型的变量持有引用值,所谓引用,即对某个对象的引用。原始值,则是真正的原始数据。
特别注意returnAddress,这种类型只在虚拟机内部使用,JAVA程序员不能使用这个类型,这个类型主要用来实现finally子句。
类类型是对类的某个实例的引用;数组类型是对数组对象(数组是一个对象)的引用;接口类型则是对实现了该接口的某个实例的引用;还有一种特殊的引用值是null,表示该引用变量没有引用任何对象。
字,用来容纳数据类型的值,它可能是32位或64位,我们无需关心,也无法通过程序来获知某个JAVA虚拟机的字长是多少,它是JAVA虚拟机的内部实现。
两种类装载器:启动类装载器和用户自定义的类装载器,启动类装载器由JAVA虚拟机实现,而用户自定义的类装载器需要继承ClassLoader类。对每一个被装载的类型,虚拟机都会创建一个java.lang.Class类型的对象,这个对象和ClassLoader对象,跟其它普通对象一样,也是存放在堆中的,而装载的类型信息,当然位于方法区中。
l 装载、连接和初始化
装载 – 读取class文件
连接 – 包括验证、准备和解析
验证 – 保证类型的正确性
准备 – 为类变量分配内存,并将其初始化为默认值
解析 – 把类型中的符号引用转换为直接引用
初始化 – 把类变量初始化为正确的初始值
l 用户自定义类装载器
继承的ClassLoader类中,四个方法非常重要:
protected final Class<?> defineClass(String name, byte[] b, int off, int len) protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) protected final Class<?> findSystemClass(String name) protected final void resolveClass(Class<?> c)
|
defineClass方法需保证能够把类型导入到方法区中
findSystemClass方法是使用系统类装载器来装载指定的类型,name是一个全限定类名
resolveClass则对指定的类型执行连接动作
类装载器装载一个类之后,把其中的类型信息存储到方法区中,类变量(即静态变量)也是存储到方法区中的。
由于所有线程共享方法区,所以对方法区中的数据的访问,必须是线程安全的,比如假如两个线程同时访问一个名为Hello的类,而这个类还没有被加载到方法区,那么只有一个线程可以去加载这个类,而另外一个线程必须等待。
方法区也可以被垃圾收集(假设某个类不再被引用)。
那么,虚拟机会在方法区中存储某个类的哪些类型信息呢?
包括如下基本类型信息:
l 全路径类名(或称全限定名)
l 这个类型的直接超类的全路径类名(除非它是java.lang.Object,因为Object没有超类)
l 这个类型是类还是接口
l 这个类型的访问修饰符(public、abstract或final或其它的修饰符)
l 任何直接超接口的全限定名的有序列表
除了基本类型信息,还存储如下信息:
l 该类型的常量池
n 常量池,就是该类型所用常量的一个有序集合。包括直接常量和对其它类型、字段、方法的符号引用。
l 字段信息
n 字段名
n 字段类型
n 字段的修饰符(public、private、protected、static、final、volatile、transient的某个子集)
l 方法信息
n 方法名
n 方法的返回类型(或void)
n 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)
u 如果这个方法不是abstract或native,那么还必须保存下列信息:
u 方法的字节码(bytecodes)
u 操作数栈和该方法的栈帧中局部变量区的大小
u 异常表
l 除了常量以外的所有类变量(静态变量)
n 虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间
n 而编译时常量(即用final声明的类变量),处理方式不同。虚拟机会将这些final声明的常量复制到使用它的常量池或字节码流中。
l 一个到类ClassLoader的引用
n 主要是因为虚拟机需要跟踪这个类究竟是由哪些类装载器装载的。
n 虚拟机会在动态连接期间使用这个信息(比如一个类引用了另外一个类,那么虚拟机必须使用此类的类装载器来装载另外那个类)
l 一个到类Class的引用
n 对于每一个被装载的类型(不管是Class还是interface),虚拟机都会给它创建一个java.lang.Class类的实例(为了创建这个实例,所以它必须拥有一个到java.lang.Class的引用)。而且,虚拟机还会把这个实例和方法区中的类型信息关联起来。
n 你可以调用Class类中的forName(String className)静态方法,来获得任何类的Class实例的引用。如果虚拟机无法装载指定的类型,将抛出ClassNotFoundException异常
n 更多如何获取一个类的Class实例的方法,在后面介绍
n Class中某些关键方法的说明:
u getSuperClass – 返回直接超类的Class实例,如果是java.lang.Object或接口,这个方法将返回null
u isInterface – 判断一个类型是否是接口
u Class[] getInterfaces() – 返回该类型实现的直接超接口(非间接实现),如果没有,则返回长度为0的数组。
u getClassLoader() – 这个方法返回该类型的ClassLoader对象的引用,如果这个类型是由启动类装载器装载的,则这个方法返回null
u 以上所有这些信息,都直接从方法区中获得
方法表
这是为了尽可能提高访问效率而设计。虚拟机为每个非抽象类,都生成一个方法表,方法表是一个数组,它的元素是这个类型的所有实例方法(包括继承下来的所有实例方法)的直接引用。
堆存放类的实例。那么一个类的实例具体包括哪些信息呢?
基本信息:
l 所有实例变量(包括从父类中继承下来的实例变量)
l 指向方法区的指针
l 对象锁 – 用来实现多个线程对共享数据的互斥访问
l 等待集合(wait set)的数据
n 每个类都从Object继承了三个等待方法(三个重载的wait方法)和两个通知方法(notify和notifyAll)
n 当某个线程在一个对象上调用等待方法时,虚拟机就阻塞这个线程,并把它放到了对象的等待集合中,直到另外一个线程在同一个对象上调用通知方法
l 与垃圾收集器有关的数据(比如标志一个对象是否仍然被引用等)
数组的内部表示:
数组也是一个对象,数组也有一个与它们的类相关联的Class的实例,所有具有相同维度和类型的数组都是同一个类的实例,而不管数组的长度。每一维用“[”表示,类型用字符或字符串表示,如int一维数组,类的名称为:[I,三维byte数组表示为“[[[B”,一维java.lang.Object数组表示为“[Ljava/lang/Object”。多维数组表示为数组的数组。比如int二维数组,表示为一个一维数组,其中的每个元素是指向一个一维数组的引用:
每个线程一个程序计数器,里面存放的就是下一条要执行的指令的地址或returnAddress
每个线程一个栈,每个方法调用一个栈帧。
方法可以有两种方式返回:
一种是通过return或执行完,正常返回
一种是抛出异常终止
不管哪种方法返回,虚拟机都会弹出栈帧。
包括:局部变量区、操作数栈、帧数据区
l 局部变量区存放局部变量和方法参数
public class Example { public static int someClassMethod(int i,long l, float f,double d,Object o,byte b){ return 0; }
public int someInstanceMethod(char c,double d, short s,boolean b){ return 0; } } |
上述方法在执行时对应栈帧的信息如下:
观察runInstanceMethod()方法的栈帧,其中有一个引用类型,它就是隐含的this,指向这个方法所属的对象,而runClassMethod()方法中则没有这个引用。因为类方法只与类有关,与具体的实例无关,所以它不可能访问到this变量!
byte/short/char/boolean都转换成了int进行存储。
对象的传递是引用传递,即在局部变量区中永远不会有对象,而仅仅只是一个引用。
对于方法的参数,严格按照参数的先后顺序来存放
l 操作数栈
操作数,即指令所携带的数据,它也放在栈帧中(但不是通过索引来访问操作数,而是通过压栈出栈的方式来使用操作数)。虚拟机指令主要从操作数栈中获取操作数(也可以从字节码流或常量池中获取)。
如:
int i = 100;
int j = 98;
int k = i + j;
对应的指令序列:
指令序列对应的操作结果:
l 帧数据区,需存储数据以支持下面的操作
a) 常量池解析
i. 很多指令要用到常量池中的数据,在常量池中,一开始只是一些符号引用,当指令要用到这些数据的时候,必须对符号引用进行解析
b) 正常方法返回
i. 方法返回时,必须将返回值压入到调用方法的操作数栈中。
c) 异常派发机制
i. 为了处理异常,在帧数据区,需保存对一个异常表的引用。
1. 异常表的每一项,包括:
2. try内部的代码的起始和结束位置
3. catch异常类在常量池中的索引
4. catch内代码的起始位置
d) 帧数据区可能还会保存一些与调试有关的信息
如:
调用方法前,把1和88.88压入操作数栈,调用方法后,创建了一个新的栈帧,而且把1和88.88放到了新的栈帧的局部变量区,调用完成后,结果被放到了上一个方法的操作数栈中。
在调用方法addTowTypes之前,需要到常量池中查找这个方法的地址,如果是第一次调用,它还是一个符号引用,所以,需要把符号引用转换为直接引用!
忽略
每个线程都会对应一个执行引擎。(后台的垃圾收集线程除外)
l 指令集
a) 操作码+操作数
b) 操作码是否需要操作数,操作码本身就已经决定了它是否需要操作数
c) 操作数可以紧跟操作码,或从常量池、局部变量区、操作数栈中获取