您的位置:首页 > 编程语言 > Java开发

《深入java虚拟机》学习笔记(第五章 java虚拟机)

2012-09-03 11:07 274 查看
第五章 JAVA虚拟机

1.1.1 初始线程
Java程序中初始的main()方法,作为该程序初始线程的起点。任何其它的线程,都是有这个初始线程启动的。
在JAVA虚拟机内部,有两种类型的线程:守护线程和非守护线程(实时线程)。比如执行垃圾收集任务的线程,就是一种守护线程。我们也可以把我们自己创建的线程标记为守护线程。初始线程,不是守护线程。
当虚拟机中所有的实时线程都结束的时候,虚拟机将自动退出(当然,也可以调用Runtime或System类中exit()方法来退出虚拟机)
如果main()方法执行完毕返回,而且在其中并没有启动其它的实时线程,那么唯一的一个实时线程(即初始线程)结束,这样,虚拟机就自动退出了。
1.2 Java虚拟机的体系结构



Java虚拟机运行一个程序,需要内存来存储许多东西,例如:字节码、从已转载的class文件中得到的其它信息、程序创建的对象、传递给方法的参数、返回值、局部变量以及运算的中间结果等等。Java虚拟机把这些数据都放到运行时数据区中。
某些运行时数据区是由程序中所有线程共享的,还有一些则只能由一个线程拥有,每个Java虚拟机实例都有一个方法区以及一个堆,它们是由改虚拟机实例中所有线程共享的。当虚拟机装载一个class文件时,它会从这个Class文件包含的二进制数据中解析类型信息。然后它把这些类型信息放到方法区中,当程序运行时,虚拟机会把所有该程序正在运行时创建的对象都放到堆中。



当每一个新线程被创建时,它都会拥有自己的PC寄存器(pc register)(程序计数器,PC是program counter的缩写)以及一个JAVA栈。如果线程正在执行的是一个JAVA方法(非本地方法),PC寄存器中的值将总是指示下一条被执行的指令,而它的JAVA栈中存储的就是这个方法调用的状态:包括局部变量、方法被调用时传进来的参数、运算的中间结果以及返回值。而本地方法(native方法,与JNI有关)调用的状态,则存储与本地方法栈或别的地方(这与具体实现有关)。
Java栈由栈帧组成,每个栈帧包含一个JAVA方法调用的状态。当线程调用一个JAVA方法时,虚拟机压入新的栈帧到该线程的栈中,当该方法返回时,此栈帧被弹出并被抛弃。



上图描述了三个线程正在执行,线程1和2在执行JAVA方法,而线程3正在执行本地方法。
1.2.1 数据类型
数据类型分两种:基本类型和引用类型,基本类型的变量持有原始值,而引用类型的变量持有引用值,所谓引用,即对某个对象的引用。原始值,则是真正的原始数据。



虽然java虚拟机把boolean看作基本类型但是当编译器把java源码编译为字节码时,它会用int或byte来表示boolean,在java虚拟机中,false是由整数0来表示,所有非0整数都是表示true,涉及boolean值得操作则会使用int。
特别注意returnAddress,这种类型只在虚拟机内部使用,JAVA程序员不能使用这个类型,这个类型主要用来实现finally子句。
Java虚拟机的引用类型有三种:类类型,接口类型以及数组类型
类类型是对类的某个实例的引用;数组类型是对数组对象(数组是一个对象)的引用;接口类型则是对实现了该接口的某个实例的引用;还有一种特殊的引用值是null,表示该引用变量没有引用任何对象。
1.2.2 字长
字,用来容纳数据类型的值,它可能是32位或64位,我们无需关心,也无法通过程序来获知某个JAVA虚拟机的字长是多少,它是JAVA虚拟机的内部实现。
1.2.3 类装载子系统
两种类装载器:启动类装载器和用户自定义的类装载器,启动类装载器由JAVA虚拟机实现,而用户自定义的类装载器需要继承ClassLoader类。对每一个被装载的类型,虚拟机都会创建一个java.lang.Class类型的实例来代表该类型,这个对象和ClassLoader对象,跟其它普通对象一样,也是存放在堆中的,而装载的类型信息,当然位于方法区中。
l 装载、连接和初始化
装载 –查找并装载类型的二进制数据
连接 –执行验证、准备和解析(可选)
验证 –保证类型的正确性
准备 –为类变量分配内存,并将其初始化为默认值
解析 –把类型中的符号引用转换为直接引用
初始化 –把类变量初始化为正确的初始值
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()方法都要接受一个名为data[]的字节数组作为输入参数,并且data[offset]到data[offset +length]之间的二进制数据必须符合java class文件格式(表示一个新的可用类型)而name参数是一个字符串,指出该类型的全限定名,使用第一个是,该类型将被赋予默认的保护域,使用第二个时,保护域将由ProtectionDomain参数指定
defineClass方法需保证能够把类型导入到方法区中
findSystemClass方法是使用系统类装载器来装载指定的类型,name是一个全限定类名
resolveClass则对指定的类型执行连接动作
1.2.4 方法区
类装载器装载一个类之后,把其中的类型信息存储到方法区中,类变量(即静态变量)也是存储到方法区中的。
由于所有线程共享方法区,所以对方法区中的数据的访问,必须是线程安全的,比如假如两个线程同时访问一个名为Hello的类,而这个类还没有被加载到方法区,那么只有一个线程可以去加载这个类,而另外一个线程必须等待。
方法区也可以被垃圾收集(假设某个类不再被引用)。
那么,虚拟机会在方法区中存储某个类的哪些类型信息呢?
包括如下基本类型信息:
l 全路径类名(或称全限定名)
l 这个类型的直接超类的全路径类名(除非它是java.lang.Object,因为Object没有超类)
l 这个类型是类还是接口
l 这个类型的访问修饰符(public、abstract或final或其它的修饰符)
l 任何直接超接口的全限定名的有序列表
在java源代码中,全限定名由类所属的包的名称加一个“.”,再加上类名组成如java.lang.Object但在class文件中所有的“.”都被“/”代替变为java/lang/Object
除了基本类型信息,还存储如下信息:
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实例的引用如forName("java.util.Enumeration")则得到Enumeration对象的Class引用。如果虚拟机无法装载指定的类型,将抛出ClassNotFoundException异常另一个得到Class对象的引用方法是调用任何对象引用的getClass()方法。
n 更多如何获取一个类的Class实例的方法,在后面介绍
n Class中某些关键方法的说明:
u getSuperClass –返回直接超类的Class实例,如果是java.lang.Object或接口,这个方法将返回null
u isInterface() –判断一个类型是否是接口
u Class[] getInterfaces() –返回该类型实现的直接超接口(非间接实现),如果没有,则返回长度为0的数组。
u getClassLoader() –这个方法返回该类型的ClassLoader对象的引用,如果这个类型是由启动类装载器装载的,则这个方法返回null
u 以上所有这些信息,都直接从方法区中获得

方法表
这是为了尽可能提高访问效率而设计。虚拟机为每个非抽象类,都生成一个方法表,方法表是一个数组,它的元素是这个类型的所有实例方法(包括继承下来的所有实例方法)的直接引用。
1.2.5 堆
堆存放类的实例。那么一个类的实例具体包括哪些信息呢?
基本信息:
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二维数组,表示为一个一维数组,其中的每个元素是指向一个一维数组的引用:



1.2.6 程序计数器
每个线程一个程序计数器,里面存放的就是下一条要执行的指令的地址或returnAddress
1.2.7 Java栈
每个线程一个栈,每个方法调用一个栈帧。
方法可以有两种方式返回:
一种是通过return或执行完,正常返回
一种是抛出异常终止
不管哪种方法返回,虚拟机都会弹出栈帧。
1.2.8 栈帧
包括:局部变量区、操作数栈、帧数据区
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;
对应的指令序列:



指令序列对应的操作结果:



帧数据区,需存储数据以支持下面的操作
a) 常量池解析
i. 很多指令要用到常量池中的数据,在常量池中,一开始只是一些符号引用,当指令要用到这些数据的时候,必须对符号引用进行解析
b) 正常方法返回
i. 方法返回时,必须将返回值压入到调用方法的操作数栈中。
c) 异常派发机制
i. 为了处理异常,在帧数据区,需保存对一个异常表的引用。
1. 异常表的每一项,包括:
2. try内部的代码的起始和结束位置
3. catch异常类在常量池中的索引
4. catch内代码的起始位置
d) 帧数据区可能还会保存一些与调试有关的信息
如:





调用方法前,把1和88.88压入操作数栈,调用方法后,创建了一个新的栈帧,而且把1和88.88放到了新的栈帧的局部变量区,调用完成后,结果被放到了上一个方法的操作数栈中。

在调用方法addTowTypes之前,需要到常量池中查找这个方法的地址,如果是第一次调用,它还是一个符号引用,所以,需要把符号引用转换为直接引用!

1.2.9 本地方法栈

忽略

1.2.10 执行引擎

每个线程都会对应一个执行引擎。(后台的垃圾收集线程除外)

l 指令集

a) 操作码+操作数

b) 操作码是否需要操作数,操作码本身就已经决定了它是否需要操作数

c) 操作数可以紧跟操作码,或从常量池、局部变量区、操作数栈中获取
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐