JVM概述

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 本地方法接口

  

原文地址:https://www.cnblogs.com/wdp1990/p/11236853.html