《深入Java虚拟机学习笔记》- 第5章 Java虚拟机

 

1.     第五章 JAVA虚拟机

 

1.1.1           初始线程

Java程序中初始的main()方法,作为该程序初始线程的起点。任何其它的线程,都是有这个初始线程启动的。

 

JAVA虚拟机内部,有两种类型的线程:守护线程和非守护线程(实时线程)。比如执行垃圾收集任务的线程,就是一种守护线程。我们也可以把我们自己创建的线程标记为守护线程。初始线程,不是守护线程。

 

当虚拟机中所有的实时线程都结束的时候,虚拟机将自动退出(当然,也可以调用RuntimeSystem类中exit()方法来退出虚拟机)

 

如果main()方法执行完毕返回,而且在其中并没有启动其它的实时线程,那么唯一的一个实时线程(即初始线程)结束,这样,虚拟机就自动退出了。

 

1.2  Java虚拟机的体系结构

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第1张图片

<本图片来源于:http://www.artima.com/insidejvm/ed2/jvm2.html >

 

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第2张图片 

Java虚拟机运行一个程序,需要内存来存储许多东西,例如:字节码、从已转载的class文件中得到的其它信息、程序创建的对象、传递给方法的参数、返回值、局部变量以及运算的中间结果等等。Java虚拟机把这些数据都放到运行时数据区中。

 

方法区和堆中的数据,是所有线程共享的区域。方法区存储的是已经加载的类的信息。在程序运行过程中所创建的所有对象,均放到堆中。

 

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第3张图片《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第4张图片 

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

 

Java栈由栈帧组成,每个栈帧包含一个JAVA方法调用的状态。当线程调用一个JAVA方法时,虚拟机压入新的栈帧到该线程的栈中,当该方法返回时,此栈帧被弹出并被抛弃。

 

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第5张图片《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第6张图片

上图描述了三个线程正在执行,线程12在执行JAVA方法,而线程3正在执行本地方法。

 

1.2.1           数据类型

数据类型分两种:基本类型和引用类型,基本类型的变量持有原始值,而引用类型的变量持有引用值,所谓引用,即对某个对象的引用。原始值,则是真正的原始数据。

 

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第7张图片《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第8张图片 

特别注意returnAddress,这种类型只在虚拟机内部使用,JAVA程序员不能使用这个类型,这个类型主要用来实现finally子句。

 

类类型是对类的某个实例的引用;数组类型是对数组对象(数组是一个对象)的引用;接口类型则是对实现了该接口的某个实例的引用;还有一种特殊的引用值是null,表示该引用变量没有引用任何对象。

 

1.2.2           字长

字,用来容纳数据类型的值,它可能是32位或64位,我们无需关心,也无法通过程序来获知某个JAVA虚拟机的字长是多少,它是JAVA虚拟机的内部实现。

 

1.2.3           类装载子系统

 

两种类装载器:启动类装载器和用户自定义的类装载器,启动类装载器由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则对指定的类型执行连接动作

 

1.2.4           方法区

 

类装载器装载一个类之后,把其中的类型信息存储到方法区中,类变量(即静态变量)也是存储到方法区中的。

 

由于所有线程共享方法区,所以对方法区中的数据的访问,必须是线程安全的,比如假如两个线程同时访问一个名为Hello的类,而这个类还没有被加载到方法区,那么只有一个线程可以去加载这个类,而另外一个线程必须等待。

 

方法区也可以被垃圾收集(假设某个类不再被引用)。

 

那么,虚拟机会在方法区中存储某个类的哪些类型信息呢?

包括如下基本类型信息

l           全路径类名(或称全限定名)

l           这个类型的直接超类的全路径类名(除非它是java.lang.Object,因为Object没有超类)

l           这个类型是类还是接口

l           这个类型的访问修饰符(publicabstractfinal或其它的修饰符)

l           任何直接超接口的全限定名的有序列表

 

除了基本类型信息,还存储如下信息:

l           该类型的常量池

n         常量池,就是该类型所用常量的一个有序集合。包括直接常量和对其它类型、字段、方法的符号引用。

l           字段信息

n         字段名

n         字段类型

n         字段的修饰符(publicprivateprotectedstaticfinalvolatiletransient的某个子集)

l           方法信息

n         方法名

n         方法的返回类型(或void

n         方法的修饰符(publicprivateprotectedstaticfinalsynchronizednativeabstract的某个子集)

u        如果这个方法不是abstractnative,那么还必须保存下列信息:

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        以上所有这些信息,都直接从方法区中获得

 

方法表

 

这是为了尽可能提高访问效率而设计。虚拟机为每个非抽象类,都生成一个方法表,方法表是一个数组,它的元素是这个类型的所有实例方法(包括继承下来的所有实例方法)的直接引用。

 

1.2.5          

堆存放类的实例。那么一个类的实例具体包括哪些信息呢?

基本信息:

l           所有实例变量(包括从父类中继承下来的实例变量)

l           指向方法区的指针

l           对象锁 用来实现多个线程对共享数据的互斥访问

l           等待集合(wait set)的数据

n         每个类都从Object继承了三个等待方法(三个重载的wait方法)和两个通知方法(notifynotifyAll

n         当某个线程在一个对象上调用等待方法时,虚拟机就阻塞这个线程,并把它放到了对象的等待集合中,直到另外一个线程在同一个对象上调用通知方法

l           与垃圾收集器有关的数据(比如标志一个对象是否仍然被引用等)

 

数组的内部表示:

数组也是一个对象,数组也有一个与它们的类相关联的Class的实例,所有具有相同维度和类型的数组都是同一个类的实例,而不管数组的长度。每一维用“[”表示,类型用字符或字符串表示,如int一维数组,类的名称为:[I,三维byte数组表示为“[[[B”,一维java.lang.Object数组表示为“[Ljava/lang/Object”。多维数组表示为数组的数组。比如int二维数组,表示为一个一维数组,其中的每个元素是指向一个一维数组的引用:

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第9张图片 

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;

    }

}

 

上述方法在执行时对应栈帧的信息如下:

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第10张图片 

观察runInstanceMethod()方法的栈帧,其中有一个引用类型,它就是隐含的this,指向这个方法所属的对象,而runClassMethod()方法中则没有这个引用。因为类方法只与类有关,与具体的实例无关,所以它不可能访问到this变量!

 

byte/short/char/boolean都转换成了int进行存储。

 

对象的传递是引用传递,即在局部变量区中永远不会有对象,而仅仅只是一个引用。

 

对于方法的参数,严格按照参数的先后顺序来存放

 

l           操作数栈

操作数,即指令所携带的数据,它也放在栈帧中(但不是通过索引来访问操作数,而是通过压栈出栈的方式来使用操作数)。虚拟机指令主要从操作数栈中获取操作数(也可以从字节码流或常量池中获取)。

如:

int i = 100;

int j = 98;

int k = i + j;

对应的指令序列:

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第11张图片 

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

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第12张图片 

l           帧数据区,需存储数据以支持下面的操作

a)       常量池解析

                     i.              很多指令要用到常量池中的数据,在常量池中,一开始只是一些符号引用,当指令要用到这些数据的时候,必须对符号引用进行解析

b)       正常方法返回

                     i.              方法返回时,必须将返回值压入到调用方法的操作数栈中。

c)       异常派发机制

                     i.              为了处理异常,在帧数据区,需保存对一个异常表的引用。

1.       异常表的每一项,包括:

2.       try内部的代码的起始和结束位置

3.       catch异常类在常量池中的索引

4.       catch内代码的起始位置

d)       帧数据区可能还会保存一些与调试有关的信息

如:

 

 

 

《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第13张图片《深入Java虚拟机学习笔记》- 第5章 Java虚拟机_第14张图片 

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

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

 

1.2.9           本地方法栈

忽略

 

1.2.10       执行引擎

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

 

l           指令集

a)       操作码+操作数

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

c)       操作数可以紧跟操作码,或从常量池、局部变量区、操作数栈中获取

你可能感兴趣的:(《深入Java虚拟机学习笔记》- 第5章 Java虚拟机)