1. JVM是一个规范来定义的抽象计算机,本质上就是运行在计算机上的软件。
可以这样理解 :
抽象规范;
具体的实现;
运行中的虚拟机实例。
JVM是基于栈来操作的虚拟就计算机,而不是基于寄存器的。
2.JVM的生命周期
一个java程序,一个JVM实例。
随着Java程序的启动 而 启动一个JVM实例,随着Java程序的 退出/关闭 而 消亡一个JVM实例。
main()是JVM的入口方法:JVM通过调用main()来运行一个Java程序。--- public static void main (String arg[]){}
main():是一个非守护线程。
线程 分类 : 守护线程 ,非守护线程。
守护线程 :
守护线程通常是JVM自己使用的(例如:垃圾回收线程);JVM退出后守护线程就是自动消亡。
可以理解:守护线程是非守护线程的附属,也就意味着一个JVM实例中的全部非守护线程结束而结束。
只要还有任何非守护线程运行,Java程序就在继续运行,JVM也就继续存活。
3.JVM的体系结构
JVM的行为:类加载器子系统 ,内存区,数据类型,指令。
运行时数据区 :存放管理数据(字节码,class文件信息,对象,方法参数,方法返回值,局部变量,中间结果等)
每一个JVM实例都有一个方法区和一个堆,他们被该JVM实例所有线程共享。
方法区 : 存放类信息。
堆 : 存放对象。
一个线程 一个PC寄存器(程序计数器)和一个栈。
当执行非本地方法时 :PC寄存器(的值)总是指向下一条将被执行的指令,栈存储方法调用的状态(局部变量,参数,返回值,中间结果等)。
当执行本地方法时,方法调用状态存储在本地方法栈中。
栈帧 : 是栈的基本单元,一个栈帧是一个Java方法调用的状态。栈帧是后进先出的,有出栈/压栈操作。
JVM没有寄存器,指令集使用Java栈来存储中间数据。
在上图中,Java栈是向下生长的,栈顶显示在图的底部,正在执行的方法栈帧以浅色表示,PC计数器指向下一条指令。
线程1 和线程2 (执行Java方法)的栈帧 :上图左边的示例;
线程3(执行本地方法) 的栈帧 :上图右边的示例;
3.1 JVM数据类型
基本类型:真正的数据 ; 应用类型 : 对象的引用。
注意 : JVM对boolean看作是基本类型,但是指令集对boolean支持有限。
编译器 把boolean 编译成 int/byte 类型。
false: 整数0 表示;true :非0 整数表示;
涉及boolean操作:转化为 int 进行操作
boolean 数组 :当作 byte数组
returnAddress :基本类型 ,只能JVM内部使用 ,用来实现Java程序中的 finally语句 。
在JVM中,数组是真正的对象 ;
接口时对实现了该接口的某一个实例的引用
类 是对类实例的引用。
注意 : 在栈中的操作都是以字长为单位的。意味着所有的不足字长的数据类型将转化为字长来操作。
例如 : byte short char boolean 将转换为inte 后,然后再栈中操作。
方法区 和堆空间 不会有这样的转换。
3.2 JVM的字长
JVM最基本的数据操作单位:字(word)
JVM设计者至少选择32位作为字长 。(通常根据底层主机的指针长度来选择字长)
运行时数据区的大部分内容操作,都是基于 字 这个概念的。
3.3 JVM 类装载器子系统
负责查找,装载类的
分类 : 启动(引导)类装载器 ,用户自定义类装载器。 每一个类装载器都有自己的命名空间,命名空间用来保护类安全的。
装载类的步骤 :
1) 装载----查找并装载类的二进制数据,最终形成该类的Class对象。
2)连接 --执行验证,准备,解析
验证 :确保类型的正确性
准备 :为类变量分配内存,并初始化默认值
解析 :把类型中的符号引用转换为直接引用。
3)初始化 --把类变量初始化为正确初始值。
启动类装载器 :是JVM实现的一部分,JVM必须有一个启动类装载器。只装载JDK安装目录下的类,且这些类不会被卸载。
自定义类装载器 : 是Java程序的一部分。它是派生自java.lang.ClassLoader。
例如 : 系统类装载器(属于自定义类装载器):装载Java程序classpath下的类,这些类可能会被卸载。
ClassLoader的4个方法是通往JVM的通道 :
defineClass( ) :任何JVM实现必须保证defineClass( ) 能够把新类型导入到方法区中。
findSystemClass( ) :任何JVM实现必须保证findSystemClass( ) 能够调用系统类装载器,此类返回一个Class对象
resolveClass( ) :任何JVM实现必须保证resolveClass( )能够让类装载器子系统执行连接动作。
任何JVM实现都必须把这些方法连到内部的类装载器子系统中。
命名空间 : 任何类装载器都有自己的命名空间,其中维护着有它装载的类。命名空间是解析过程的结果。
3.4 方法区
存放类型信息的内存。从类的二进制数据中,提取类型数据放到方法区中。
当JVM运行Java程序时,就会查找使用存储在方法区中的类型信息。
由于方法区是线程共享的,所以方法区数据的访问必须被设计成线程安全的。
方法区的大小不必是固定的,可以动态扩展或收缩, 也不必是连续的。
方法区可以被垃圾回收。
方法区存放的类型信息 :
类信息 : 全限定名 , 访问修饰符 , 是类 还是接口 , 直接父类的全限定名 , 直接接口全限定名的有序列表
常量池 : JVM 必须为每一个被装载的类维护一个常量池,池中数据项通过索引访问
字段信息 : 字段名 ,字段类型 ,字段修饰符
方法信息 : 方法名 ,方法修饰符 ,方法参数,方法返回值
类变量 : 作为类信息,存储在方法区。 编译时常量 :是存储在常量池中的
到ClassLoader的引用 : 装载此类的ClassLoader
到Class类的引用 : 此类的Class对象
方法表 : 是数组,它的元素是实例对象的实例方法的直接引用,包含从父类继承的实例方法。 运行时可以快速访问实例方法
包含信息 : 方法的操作数栈,局部变量区,方法字节码,异常表
3.5 堆
存放对象信息的。
JVM 有一条在堆中分配内存的指令,但是没有释放内存的指令。
垃圾回收器 :负责回收对象,释放内存。
堆空间的大小不必是固定的,可以动态扩展或收缩, 也不必是连续的。
3.5.1 对象的表示 :如何在堆中表示对象。无论如何表示,都必须有个指向方法区的指针。
对象表示法一 : 一个语柄池,一个对象池。
优点 : 利于碎片整理;缺点 :每次访问需要两次指针操作,效率低下
对象表示法二 :维护一组数据表。
优缺点和上面相反。
不管JVM如何表示对象,很有可能每一个对象都有一个指向方法区方法表的指针,可以加快访问 实例方法 效率。
对象中的锁对象 : 每一个对象都有一个对象锁。
只有当第一次需要加锁时才给对象分配对应的锁数据,即对象内有一个指向锁的指针。
但是对象在其生命周期内没有使用锁,就没必要与锁关联。
等待集合(wait set) : 就是让多线程完成一个共同的目标而协调工作的。
等待集合由等待方法和通知方法联合使用 wait() / notfy()。
当某一个线程在一个对象上调用wait()时,JVM阻塞线程,并把线程放入等待集合中。
当另一个线程在同一个对象上调用notify( ) 时,等待集合中的线程被唤醒,进入准备阶段。
只有当第一次需要 wait set 时才给对象分配对应的 wait set,即对象内有一个指向 wait set 的指针。
但是对象在其生命周期内没有使用 wait set ,就没必要与锁关联。
垃圾回收相关的数据 :垃圾回收器必须跟踪程序中引用的对象,这个任务不可避免的给对象增加了额外数据。
此外对于不在引用的对象,还需要指明他的终结方法 -- finalizer( ) 是否运行过。
垃圾回收器只能执行一次对象的 finalizer( ) ,但允许 finalizer( ) 复活对象,即在此被引用;当该对象再次被回收时,
就不会执行 finalizer( ) 。
3.5.2 数组的内部表示
数组类的名称由两部分组成: 维度 :使用 “[” 表示,元素类型 :使用字符表示。
int[] : [I ; int[][] : [[I ; Object[][] :[[Ljava/lang/Object
3.6 PC 程序计数器
每一个线程拥有一个PC计数器。线程启动时创建,长度时一个字长。
3.7 栈
每一个线程分配一个栈,栈上数据时线程私有的。
两种操作 : 压栈 出栈(这两种操作均以栈帧为单位的)
Java方法的两种方式返回 : 正常返回(return);异常返回。
Java方法返回,栈帧被弹出。
栈 不必时连续的。
3.8 栈帧
组成 :局部变量区 ,操作数栈, 帧数据区。他们的大小是以字长计算的。
3.8.1 局部变量区
是以字长为单位的组数,从0 开始计数,通过索引访问 ;包含了对应方法的参数和局部变量。
class Example3a{ public static int runClassMethod ( int i ,long l,float f,double d,Object o,byte b ) { return 0; } public int runInstanceMethod ( char c ,double d,short s,boolean b ) { return 0; } }
在实例方法的栈帧中第一项是this引用,这是隐含加入的;类方法就存在此引用。
byte ,short ,char , boolean 在局部变量区被转换为inte ,在操作数栈也是同样转换为int 。
注意一点 :在方法区和堆空间都不会被转换。
Java方法的参数(形参)会严格按照声明的顺序存放,而真正的局部变量可以任意放置。
3.8.2 操作数栈
是以字长为单位的数组,但不是通过索引访问,而是通过 压栈 和出栈来访问的。
操作数栈和局部变量区存储数据的方式是一样的,数据会转化为int类型来操作。
JVM指令是从操作数栈中读取操作数 ,而不是从寄存器中读取操作数。因此,JVM是基于栈的。
JVM 把操作数栈作为它的工作区----大多数指令都是从操作数栈弹出数据,执行运算;然后再把结果压回操作数栈中。
3.8.3 帧数据区
常量池的解析,正常方法返回,异常派发的一些数据。
每当执行 要访问常量池中数据 的指令时,就需要帧数据区中指向常量池的指针,来找到数据。
Java方法正常返回时 :就是Java方法结束时,JVM需要恢复 发起调用的方法的栈帧(上一个栈帧),
设置PC寄存器指向发起调用的方法的指令,
把方法返回值压入发起调用的方法的操作数栈中。
Java方法异常退出时:帧数据必须存放一个对此方法异常表的引用,JVM根据异常表来决定如何处理异常。
其他数据也可存放在帧数据区。例如 调试数据。
3.9 本地方法栈
3.10 执行引擎
JVM规范中,执行引擎的行为使用指令集定义。
运行中的Java程序的每一个线程都有一个独立的执行引擎实例。
所有属于用户运行程序的线程,都是在实际工作的执行引擎。
指令集 :
方法的字节码流是由JVM指令序列构成的。
每一条指令包含一个字节的操作码,后面跟随n个操作数。
操作码 就是将要执行的动作,操作数 就是操作码需要的额外信息。
指令集关注的重心时操作数栈。
指令集实际的工作方式就是把局部变量当作寄存器,用索引来访问。
执行技术 : 解释 ,即时编译 ,自适应优化,芯片级直接执行。
3.11 本地方法接口