• 1. Java虚拟机 张娇 2008.11.21
  • 2. 前 言
  • 3. Java及Java虚拟机 说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系如下图所示:
  • 4. JAVA四个方面关系
  • 5. 为什么要使用Java虚拟机
  • 6. 为什么要使用Java虚拟机Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。 它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现; 在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性.
  • 7. 为什么要使用Java虚拟机
  • 8. 为什么要使用Java虚拟机 一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用模式Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。
  • 9. 主要内容JVM的体系结构 Java class文件格式 JVM类装载体系 JVM垃圾收集机制
  • 10. PART 1:JVM的体系结构
  • 11. JVM内部体系结构
  • 12. 类装载器子系统定位和导入二进制class文件到JVM运行数据区中,并负责验证导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。这些动作必须严格按照装载-连接(验证、准备和解析)-初始化的顺序进行。 Java虚拟机有两种类装载器:启动类装载器和用户自定义类型装载器。每个JVM实现都必须有一个启动类装载器,它知道如何去装载受信任的类。
  • 13. 方法区(method area) 方法区是所有线程共享的,因此它们对方法区数据的访问必须被设计为是线程安全的。 方法区存储类的基本信息、常量池、静态变量、字段信息、方法信息、一个到类ClassLoader的引用,一个到Class类的引用。 方法区大小不是连续固定的,并且方法区中的数据是允许垃圾收集的,如一些类不再被程序引用,JVM可以卸载该类在方法区所占用的内存空间。
  • 14. 方法区代码示例class Lava { private int speed = 5; void flow() { } class Volcano { public static void main(String[] args) { Lava lava = new Lava(); lava.flow(); } } 告诉虚拟机装载Volcano这个类,虚拟机会把main方法的字节码装载到方法区。 执行main方法,在执行时,它会一直持用对当前类Volcano常量池的指针。 main方法第一条指令告知为列在常量池第一项类分配足够的内存 发现是对Lava类的一个引用,检查方法区是否装载了Lava类。 获取到Volcano的ClassLoader引用装载Lava。用指向方法区Lava类数据的指针替换常量池第一项(即Lava字符串) 获取Lava方法区信息,准备new一个Lava对象。 Lava对象引用压栈,执行Lava的Cinit方法把speed值设为5 获取Lava方法区flow方法信息,执行该方法
  • 15. 堆 (heap)JAVA程序在运行时所创建的所有类实例和数组都放在同一个堆中,每个JAVA程序都有自己独立的堆空间,但同一个JAVA程序的多个线程却共享同一个堆空间。 JVM通过new指令在堆上分配新对象,但没有释放对象的指令,堆上需要垃圾收集器进行对象回收,不过JVM规范并没有强制垃圾收集器。 堆内存中对象的内部表示,影响着整个堆效率以及内存回收机制的设计,JVM规范也没有就此进行规定,由JVM实现者决定
  • 16. PC寄存器每一个线程都有自己独立的PC寄存器,它是一个字长,可持有一个本地指针或returnAddress类型数据。 当线程执行某个JAVA方法时,PC寄存器指向的是下一个要被执行指令的地址。它可能是一个本地指针,也可能是在方法字节码中的相对地址,即偏移量。 如果线程在执行一个本地方法,PC寄存器的内容为undefined
  • 17. JAVA栈每一个线程都有自己私有的一个JAVA栈,任何线程不能调用其它线程的栈,JVM对JAVA栈只有以栈帧为单位的压栈和出栈。 当线程调用一个方法的时候,虚拟机会在该线程的JAVA栈上压入一个新帧。自然该帧成为当前帧,在执行该方法期间,线程用该帧来储存参数,局部变量和中间运算结果等。 JAVA方法执行有两种情况,一种是正常执行完成return,另一种是异常抛出中止。虚拟机都会将当前的线桢出栈。 JAVA栈实现方式可以由实现者任意实现。
  • 18. 本地方法栈JVM中某个线程调用一个本地方法时,按照所调用的本地方法接口,创建一个本地方法栈。 当本地方法需要回调JVM中的JAVA方法时,该线程会保存本地方法栈的状态进入到另一个JAVA栈。
  • 19. 执行引擎(execution engine)JVM中每一个线程都是一个独立的执行引擎的实例,从线程的开始到结束,它要么执行字节码,要么执行本地方法。 执行引擎的行为是一组指令集的定义集合,JVM的设计实现可自主决定如何执行指令集,但JVM规范指令集的处理结果。
  • 20. 指令集 方法的字节码流是由JVM的指令序列构成。每一条指令包括一个操作码及0~N个操作数。 执行一条指令包含的任务之一就是决定下一条要执行的指令,在正常情况下下一条指就是当前操作码和操作数之后的那个字节。但如遇到指令跳转的语句,如goto或return,或者异常抛出,执行引擎将跳转到非顺序的指令上。 指令集关注的中心是操作数栈,而非寄存器。这样的设计也是为了JAVA平台无关的特性,使得在那些只有很少的寄存器或寄存器没有规律的机器上也能实现JVM。 指令集具备使用数据流分析器进行一次性字节码验证的能力,而非在执行每条指令的时候进行验证,有助于提高执行速度。
  • 21. PART 2:CLASS文件格式
  • 22. Java classJava class文件是Java程序编译的二进制文件,是对Java程序的精确描述和定义。 Java class文件是以字节为单位的二进流制,数据按顺序存储在class文件中,相邻数据项没有任何间隔,使文件紧凑。 JVM从Java class文件获取运行中所需要所有类和接口消息。
  • 23. Class文件数据项基本类型u11个字节,无符号类型u22个字节,无符号类型U44个字节,无符号类型U88个字节,无符号类型
  • 24. CLASS文件数据项表类型数据项数量u4magic1u2minor_version1u2major_version1u2constant_pool_count1cp_infoconstant_poolconstant_pool_count-1u2access_flags1u2this_class1u2super_class1u2interfaces_count1u2interfacesinterfaces_countu2field_count1field_infofieldsfields_countU2method_count1method_infomethodsmethod_countu2attributes_count1attribute_infoattributesattributes_count
  • 25. Class文件各项数据项magic,每个class文件的前四个字节必须是0xCAFEBABE,该数称为魔数 minor_version和major_version,接下的四个字节为次版本号,主版本号。 接下来的两个字节是constant_pool_count,即常量池数量,constant_pool_count占据了常量池索引0。
  • 26. Class文件各项数据项—常量池接下来的是constant_pool,即常量池,常量池的数量项等于constant_pool_count-1,因为constant_pool_count占据了索引0。 常量池并不是通常意义上的常量,它其实称为符号池会更贴近事实一些,所有类名、方法名、字段名都存储在常量池中。一些final常量值也存储在常量池中。 常量池隐含索引,在解析class文件的常量池时,第一项索引1,第二项为2类推。
  • 27. Class文件各项数据项—Access_flagAccess_flag为紧接着常量池后的两个字节,该选项表明该类的访问标志。 标志名值含义限制ACC_PUBLIC0x0001Public类型类和接口ACC_FINAL0x0010Final类型只有类ACC_SUPER0x0020使用新型的Invokespecial类和接口ACC_INTERFACE0x0200接口接口ACC_ABSTRACT0x0400抽象类型类
  • 28. Class文件各项数据项—this_class和super_classthis_class和super_class均占2个字节,值为指向常量池存储类全限定名的索引。
  • 29. Class文件各项数据项—interface_count和interfaceInterface_count表明该类直接实现或者通过继承实现的父接口数量。 在interface_count后面为一组数量等于interface_count的数组,这组数据指向interface在常量池入口的索引,如果没有接口的话,该项无数据。
  • 30. Class文件各项数据项—fields_count和fields接下来的字节是fields_count,是类变量和实例变量的字段的数量总和,不管是静态还是非静态,public还是private,final还非final。均为fields,被fields_count所统计,注意的是fields不包括从父类中继承的字段。 Fields由一组field_info数据组成,数量等于fields_count。field_info包括字段的标志,简单名称,描述符,如果为final类型字段,field_info还将包括attribute信息用于指向常量值信息。
  • 31. Class文件各项数据项—method_count和methods在字段后面定义的是方法信息,首先两个字节是method_count,无论是抽象方法还是接口方法,均为method_count统计范围。但是如果是从父类继承(由父类实现)的方法,不包括在内。 Methods是由一组method_info组成的数据,数量等于method_count。method_info定义了方法的标志,方法名和描述,以及字节码,栈空间长度和异常表等信息。
  • 32. Class文件各项数据项—Attributes_count和Attributes最后部分是属性,在方法后面的两个字节是Attributes_count,它定义了后面续存几个Attributes。 Attributes由一组Attribute_info组成,数量等于Attributes_count。每个Attribute_info第一项指向Utf8常量索引,给出属性名称。 Class直接定义仅有名称为SourceCode和InnerClasses两种属性,在Field_info和Method_info中包含其它名称属性。
  • 33. PART 3: JVM类装载体系
  • 34. 类生命周期的开始JVM严格按照装载、连接和初始化的顺序进行类的装载引用。 文件格式的一些检查在装载阶段完成,虽然它逻辑上属于连接验证。 解析在大部分JVM实现中都表现为延迟解析,运行在初始化之后。
  • 35. 装载阶段通过类的全额定名,生成一个代表该类型的二进制数据。 简单检查二进制数据,确保每一个部分都在正确位置,文件不是太长或太短等。 解析二进制数据为方法区内的内部数据结构。 创建一个表示该类型的Class实例。
  • 36. 验证阶段检查final的类不能有子类 检查final方法不能被覆盖 检查类和超类,实现接口之间没有不兼容的方法声明 所有超类和实现接口在这里会被装载,但不会初始化检查所有常量入口相互一致 检查常量池中所有符号引用符合格式 检查字节码完整,不会危害JVM安全
  • 37. 准备阶段 为类变量(指非final类的静态变量)分配置内存,并设置默认值。 类型默认值int0long0Lshort(short)0char\u0000byte(byte)0booleanFalsereferenceNullfloat0.0fdouble0.0d
  • 38. 解析阶段解析是把类的常量池中的类、接口、字段和方法的符号引用解析为直接引用的过程。 通常该过程是延缓执行的,如对于字段来说,仅在新建一个类实例的情况下才会开始解析,对于方法,或方法中引用的类或接口,或者引用其它类的方法的解析,在调用该方法时才会进行解析。
  • 39. 初始化阶段class A{   static final int a = 10;   static{ System.out.println( “initializing A” );   }     }    class B{ public static void main(String[] args) {   System.out.println(A.a); }      static{   System.out.println( "initializing B" );   }  } 调用B的main函数,会得到什么输出? (1)"initializing B“ "initializing A"   10 (2)"initializing B" 10   
  • 40. 初始化阶段如果类存在对非final静态变量的赋值,或者static代码段,在编译时会隐式生成一个()方法,即初始化执行的方法体。 首先查找超类是否存在cinit方法,如果有先初始化超类。 如果多个线程同时引起类的初始化,仅仅只能有一个线程来执行,其它线程需要等待,执行线程完成初始化通知其它等待线程。 仅仅类被主动使用时才会使类被初始化,诸如调用一个类的final变量之类的被动使用不会引起初始化。
  • 41. 主动使用的六种情形创建类的新实例。 调用类中声明的静态方法。 操作类或者接口中声明的非final的静态变量。 调用Java API中特定的reflection方法。 初始化一个类的子类。 指定一个类为JVM的启动类,即运行该类定义的main方法。 java虚拟机只有在第一次主动使用一个类的情况下才会初始化该类。在以上6种情况下,虚拟机认为正在主动使用该类,所以会对其进行初始化(任何一个类的初始化都要求它的所有祖先类(非祖先接口)预先被初始化,而接口的初始化则不需要祖先接口预先被初始化。 )
  • 42. PART 4: JVM垃圾收集机制
  • 43. 垃圾收集 在JAVA虚拟堆里存放着正在运行的JAVA程序所创建的所有对象,使用new,newarray, Class的newInstant,clone等指令来创建对象.但是没有明确的代码来释放它们,垃圾收集就是自动释放不再被程序所使用的对象的过程. 在Java虚拟机的规范中没有严格定义垃圾收集,只是定义一个Java虚拟机的实现必须通过某种方式管理自己的堆。
  • 44. 对象的回收和终结当对一个对象所有的引用都置为null,意味着该对象成为不再使用的垃圾数据。 对象的终结在回收前进行,终结方法finalize引发的任何异常都会被忽略。 对象终结方法可能引起对象重新被使用。
  • 45. THANK YOU!