深入虚拟机笔记之java虚拟机

 2019-12-22 11:02  阅读(702)
文章分类:JVM

第5章 java虚拟机

java虚拟机实例通过调用某个初始类的main()方法来运行一个java程序。这个main()方法签名必须是public static void main(String[] args)。

基本类型:持有原始数据。

引用类型:持有引用值,引用值指的是对某个对象的引用,而不是该对象本身。

java中的所有基本类型同样也是java虚拟机中的基本类型。但boolean有点特别,当编译器把java源码编译为字节码时,它会用int或byte来表示boolean。java虚拟机中,false由整数0表示,所有非零整数表示true。

涉及boolean的操作则会使用int,boolean数据是当byte数据来访问的,在堆中也可以被表示为位域。

java虚拟机的引用类型被统称为引用(reference),由三种引用类型:类、接口和数组。它们的值是对动态创建对象的引用。还有一种特殊的引用值是null,它表示引用变量没有引用任何对象。

每个java虚拟机都有一个类装载器子系统,它根据给定的全限定名来装入类型(类或接口)。同样每个java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。

类装载子系统除了要定位和导入class字节码文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。

这些动作必须严格按照顺序进行:

(1) 装载:查找并装载类的字节码数据。

(2) 连接:执行验证、准备以及解析(可选)。

验证:确保被导入类的正确性。

准备:为类变量分配内存,并将其初始化为默认值。

解析:把类型中的符号引用转换为直接引用。

(3) 初始化:把类变量初始化为正确的初始值。

运行时数据区:方法区、堆、栈、PC寄存器、本地方法栈。
2019120001496\_1.png

每个java虚拟机实例都有一个方法区和一个堆,它们是由该虚拟机实例中的所有线程共享的。当虚拟机装载一个class文件时,它会从这个class文件中包含的字节码数据中解析类型信息。然后,它把这些类型信息放到方法区中。当运行程序时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。

每当一个线程被创建时,它都将得到它自己的PC寄存器(程序计数器)以及一个java栈。如果线程正在执行的是一个java方法(非本地方法),那么PC寄存器的值总是指向下一条将被执行的指令,而它的java栈则总是存储该线程中的java方法调用的状态(包括局部变量、调用参数、返回值以及运算的中间结果等)。

java栈是由许多栈帧(stack frame)组成的,一个栈帧包含一个java方法调用的状态。当线程调用一个方法时,虚拟机压入一个新的栈帧到该线程的java栈中;当该方法返回时,这个栈帧被弹出并抛弃。java虚拟机没有寄存器,其指令集使用java栈来存储中间数据。

本地方法调用的状态,则是以某种依赖于虚拟机具体实现的方式存储在本地方法栈中。

2019120001496\_2.png

java虚拟机中,被装载类型的信息存储在一个逻辑上称为方法区的内存中。该类型中的类(static)变量同样也是存储在方法区中。

由于所有线程共享方法区,因此它们对方法区数据的访问必须被设计成为是线程安全的。

虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,包括直接常量(String、int、float等字面常量)和对其他类型、字段、方法的符号引用。常量池中的数据项就像数组一样是通过索引访问的。常量池存储了该类型所用到的所有类型、字段和方法的符号引用,所以它在java程序的动态连接中骑着核心的作用。

类(static)变量是由所有类实例共享的,即使没有任何类实例,它也可以被访问。这些变量只与类有关,因此成它们总是作为类型信息的一部分而存储在方法区。除了编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。

编译时常量(用final声明并且用编译时已知的值初始化的类变量)和一般的类变量的处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。作为常量池或者字节码流的一部分,编译时常量保存在方法区中。和类变量的区别是:类变量作为类型信息的一部分被保存;编译时常量作为使用它的类型的一部分被保存。

虚拟机对每个装载的非抽象类,都生成一个方法表,把它作为类信息的一部分保存在方法区。方法表是一个数组,它的元素是所有它的实例可能被调用的实例方法的直接引用,包括那些从超类继承过来的实例方法(对于抽象类和接口,方法表没有什么帮助,因为程序决不会生成它们的实例。)。运行时可以通过方法表快速搜寻对象调用中的实例方法。

虚拟机不会等到把程序中用到的所有类都装载后才开始运行程序。恰好相反,它只在需要时才装载相应的类。

虚拟机以一个直接指向方法区被装载类型数据的指针替换常量池中的符号引用(以后就可以使用这个指针快速地访问对应的类型信息),这个替换过程成为常量池解析。即把常量池中的符号引用替换为直接引用。

java虚拟机总能够通过存储于方法区的类型信息来确定一个对象需要多少内存。

java程序在运行时创建的所有类实例或数组都放在同一个堆中。而一个java虚拟机实例中只存在一个堆,因此所有线程都将共享这个堆。在多线程编程中需要考虑访问对象(堆数据)的同步问题。

只要有一个对象引用,虚拟机就必须能够快速地定位对象实例的数据。另外,它也必须能通过该对象引用访问相应的类型数据(存储在方法区的类型信息)。因此在对象中通常会有一个指向方法区的指针。

在java中,数组是真正的对象,数组总是存储在堆中。和其他对象一样,数组也拥有一个与它们的类相关联的Class实例,所有具有相同维度和类型的数组都是同一个类的实例,而不管数组的长度是多少。数组类的名称由两部分组成:每一维用一个方括号”[“表示,用字符或字符串表示元素类型,如一维int数组的类名为[I。多维数组被表示为数组的数组。

对于一个运行中的java程序而言,其中的每一个线程都有它自己的PC寄存器(程序计数器),它是在该线程启动时创建的。PC寄存器的大小是一个本地字长,因此它既能够持有一个本地指针,也能够持有一个returnAddress。当线程执行某个java方法时,PC寄存器的内容总是下一条将被执行指令的地址(本地指针或者相对于该方法字节码起始指令的偏移量)。如果正在执行一个本地方法,PC寄存器的值是undefined。

2019120001496\_3.png

每当启动一个新线程时,java虚拟机都会为它分配一个java栈。java栈以栈帧为单位保存线程的运行状态。虚拟机只会对java栈执行两种操作:以栈帧为单位的压栈或出栈。当虚拟机遇到栈内操作指令时,它对当前帧内的数据执行操作。

每当线程调用一个java方法时,虚拟机都会在该线程的java栈中压入一个新的栈帧。这个新帧就成为当前帧。在执行这个方法时,它使用当前帧来存储参数、局部变量、中间运算结果等数据。

java方法可以以两种方式完成。一种通过return返回的成为正常返回;一种是通过抛出异常终止的。不过以哪种方式放回,虚拟机都会将当前帧弹出java栈然后抛弃掉。

java栈上的所有数据都是该线程私有的。任何线程都不能访问另一个线程的栈数据。

栈帧由三部分组成:局部变量区、操作数栈和帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,编译器在编译时就确定了这些值并放在class文件中。而帧数据区的大小依赖于具体的虚拟机实现。

当虚拟机调用一个java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入java栈中。

java虚拟机对byte、short和char是直接支持的,这些类型的值可以作为实例变量或者数组元素存储在局部变量区,也可以作为类(static)变量存储在方法区,但在局部变量区和操作数栈中都会被转换成int类型的值。它们在栈帧中的时候都是当作int来进行处理的,只有当它被存回堆或者方法区时,才会转换回原来的类型。在java中,所有的对象都是按引用传递,并且都存储在堆中,永远都不会在局部变量区或操作数栈中发现对象的拷贝,只会有对象引用。

局部变量区和操作数栈都被组织成一个以字长为单位的数组,局部变量区以索引方式访问,而操作数栈是通过标准的栈操作(压栈、出栈)来访问的。

java虚拟机的指令是从栈中而不是从寄存器中取得操作数,因此它的运行方式是基于栈的而不是基于寄存器。虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈中。

java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些信息都保存在java栈帧的帧数据区中。

每当虚拟机执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。

当处理java方法的正常结束或异常中止时,如果是通过return正常结束,虚拟机必须恢复发起调用的方法的栈帧,设置PC寄存器指向发起调用的方法中的下一个指令。假如有返回值,必须将它压入到发起调用方法的操作数栈中。

为了处理异常退出情况,帧数据区还必须保存一个对此方法异常表的引用。当某个方法抛出异常时,根据帧数据区的异常表决定如何处理,如果找到匹配的catch子句,就会把控制权交给catch子句内的代码;如果没有发现,方法会立即异常中止,然后虚拟机使用帧数据区的信息恢复发起调用的方法的帧,然后在发起调用的方法的上下文中重新抛出同样的异常。

任何java虚拟机的线程实现都必须支持同步的两个方面:对象锁定、线程等待和通知。对象锁定使独立运行的线程访问共享数据的时候互斥;线程等待和通知使得线程为了达到同一个目标而互相协同工作。运行中的程序通过java虚拟机指令集来访问上锁机制,通过Object类的wait、notify和notifyAll方法来访问线程等待和通知机制。

每一个java虚拟机实例都有一个主存,用于保存所有的程序变量(对象的实例变量、数组的元素和类static变量)。java虚拟机规范定义了许多规则,用来管理线程和主存之间的低层交互行为。比如,所有对基本类型的操作,都必须是原子性的。例外的情况:即任何没有声明为volatile的long和double类型变量,某些实现可能把它们作为两个原子性的32位值对待,而非一个原子性的64位值。

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

相关推荐