《深入Java虚拟机学习笔记》

 2019-12-22 10:56  阅读(617)
文章分类:JVM

原文来自:http://blog.csdn.net/li_tengfei/article/details/6097977

1. 第五章 JAVA虚拟机

1.1.1 初始线程

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

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

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

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

1.2 Java虚拟机的体系结构

2019120001416\_1.png

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

2019120001416\_2.png

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

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

2019120001416\_3.png2019120001416\_4.png

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

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

2019120001416\_5.png2019120001416\_6.png

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

1.2.1 数据类型

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

2019120001416\_7.png2019120001416\_8.png

特别注意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(Stringname, byte[]b, int off, int len)protected final Class<?> defineClass(Stringname, byte[]b, int off, int len,ProtectionDomainprotectionDomain)protected final Class<?> findSystemClass(Stringname)protected final void resolveClass(Class<?>c) 
protected final Class<?> defineClass(Stringname, byte[]b, int off, int len)protected final Class<?> defineClass(Stringname, byte[]b, int off, int len,ProtectionDomainprotectionDomain)protected final Class<?> findSystemClass(Stringname)protected final void resolveClass(Class<?>c) 

defineClass方法需保证能够把类型导入到方法区中

findSystemClass方法是使用系统类装载器来装载指定的类型,name是一个全限定类名

resolveClass则对指定的类型执行连接动作

1.2.4 &sp; 方法区

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

由于所有线程共享方法区,所以对方法区中的数据的访问,必须是线程安全的,比如假如两个线程同时访问一个名为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 以上所有这些信息,都直接从方法区中获得

方法表

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

1.2.5

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

基本信息:

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

l 指向方法区的指针

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

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

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

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

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

数组的内部表示:

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

2019120001416\_9.png

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,Objecto,byte b){       return 0;    }       public int someInstanceMethod(char c,double d,           short s,boolean b){       return 0;    }}
public class Example{    public static int someClassMethod(int i,long l,           float f,double d,Objecto,byte b){       return 0;    }       public int someInstanceMethod(char c,double d,           short s,boolean b){       return 0;    }}

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

2019120001416\_10.png

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

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

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

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

l 操作数栈

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

如:

int i = 100;

int j = 98;

int k = i + j;

对应的指令序列:

2019120001416\_11.png

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

2019120001416\_12.png

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

a) 常量池解析

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

b) 正常方法返回

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

c) 异常派发机制

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

  1. 异常表的每一项,包括:
  2. &bsp; try内部的代码的起始和结束位置
  3. catch异常类在常量池中的索引
  4. catch内代码的起始位置

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

如:

2019120001416\_13.png2019120001416\_14.png

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

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

1.2.9 本地方法栈

忽略

1.2.10 执行引擎

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

l 指令集

a) 操作码+操作数

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

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

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 《深入Java虚拟机学习笔记》

相关推荐