·商业授权工具, 正式支持工具, 实验性工具
jps:虚拟机进程状况工具:可以列出正在运行的虚拟机进程
jstat:虚拟机统计信息监视工具:是用于监视虚拟机各种运行状态信息的命令行工具。
jinfo:Java配置信息工具:作用是实时查看和调整虚拟机各项参数。
jmap:Java内存映像工具:命令用于生成堆转储快照(一般称为heapdump或dump文件)。
jhat:虚拟机堆转储快照分析工具:命令与jmap搭配使用,来分析jmap生成的堆转储快照。
jstack:Java堆栈跟踪工具:用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者 javacore文件)。
可视化故障处理工具,用户可以使 用这些可视化工具以更加便捷的方式进行进程故障诊断和调试工作。
JHSDB:基于服务性代理的调试工具,JCMD和JHSDB两个集成式的多功能工具箱。JConsole:Java监视与管理控制台:是一款基于JMX(Java Manage-ment Extensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进 行信息收集和参数动态调整。
VisuaVM:多合-故障处理工具:它除了常规的运行监视、故障处理外,还将提供其他方面的能 力,譬如性能分析(Profiling)
Java Mission Control:可持续在线的监控工具:JMC与虚拟机之间同样采取JMX协议进行通信,JMC一方面作为 JMX控制台,显示来自虚拟机MBean提供的数据;另一方面作为JFR的分析工具,展示来自JFR的数据。
HotSpot虚拟机插件及工具 HSDIS是一个被官方推荐的HotSpot虚拟机即时编译代码的反汇编插件, HSDIS插件的作用是让HotSpot的-XX:+PrintAssembly指令调用它来把即时编译器动态生成的本 地代码还原为汇编代码输出,同时还会自动产生大量非常有价值的注释,这样我们就可以通过输出的 汇编代码来从最本质的角度分析问题。
类文件结构
无关性的基石: 字节码(Byte Code) 是构成平台无关性的基石。
Class类文件的结构:
任何一个Class文件都对应着唯一的一个类或接口的定义信息,类或 接口并不一定都得定义在文件里。
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文 件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数 据,这种伪结构中只有两种数据类型:“无符号数”和“表”。 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个 字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名 都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视 作是一张表,这张表由表6-1所示的数据项按严格顺序排列构成。
魔数与Class文件的版本
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件。
第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。
常量池
与Java中语言习惯不同,这个容量计数是从1而不是0开始的。常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
而符号引用则属于编译原理方面的概念,主要包括下面几类常量: 被模块导出或者开放的包(Package) ·类和接口的全限定名(Fully Qualified Name) ·字段的名称和描述符(Descriptor) ·方法的名称和描述符 ·方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic) ·动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
虚拟机类加载机制
类加载的时机:
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称 为连接(Linking)。
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。
解析阶段则不一定按照确定顺序,它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性。
第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
但是对于初始化阶段,《Java虚拟机规范》 则是严格规定了有且只有六种情况必须立即对类进行“初始化”:
1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有: ·使用new关键字实例化对象的时候。 ·读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外) 的时候。 ·调用一个类型的静态方法的时候。
2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 初始化这个主类。
5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
对于静态字段, 只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发 父类的初始化而不会触发子类的初始化。
通过数组定义来引用类,不会触发此类的初始化,创建动作由字节码指令newarray触发。
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的 类的初始化
类加载的过程
1加载:“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段。在加载阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
非数组类型的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的阶段。既可以使用Java虚拟机里内置的引导类加 载器来完成,也可以由用户自定义的类加载器去完成。
如果数组的组件类型是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将被标 识在加载该组件类型的类加载器的类名称空间上。
如果数组的组件类型不是引用类型(例如int[]数组的组件类型为int),Java虚拟机将会把数组C 标记为与引导类加载器关联。
数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的 可访问性将默认为public,可被所有的类和接口访问到。
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求。
1.文件格式验证:是保证输入的字节流能正确地解析并存储于方法区之内,格式上符 合描述一个Java类型信息的要求。
这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中进行存储,所以后面的三个验证阶段 全部是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。
2.元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
3.字节码验证:
最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定 程序语义是合法的、符合逻辑的。如果一个类型中有方法体的字节码没有通过字节码验证,那它肯定是有问题的;但如果一个方法 体通过了字节码验证,也仍然不能保证它一定就是安全的。
4.符号引用验证
最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在 连接的第三阶段——解析阶段中发生。
主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机 将会抛出一个java.lang.IncompatibleClassChangeError的子类异常。
符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,虚拟机实现可以根据需 要来自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引 用将要被使用前才去解析它。
对同一个符号引用进行多次解析请求是很常见的事情,除invokedynamic指令以外,虚拟机实现可 以对第一次解析的结果进行缓存,譬如在运行时直接引用常量池中的记录,并把常量标识为已解析状态,从而避免解析动作重复进行。
过对于invokedynamic指令,规则就不成立。因为 invokedynamic指令的目的本来就是用于动态语言支持。“动态”的含义是指必须等到程序实际运行到这条指令时,解析动作才能进行。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7 类符号引用进行。
初始化
类的初始化阶段是类加载过程的最后一个步骤,直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。:初始化阶段就是执行类构造器·<clint>()方法的过程。·<clint>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。
·<clint>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的 语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问 到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。由于父类的方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
类加载器:Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动 作的代码被称为“类加载器”(Class Loader)。
类与类加载器:
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每 一个类加载器,都拥有一个独立的类名称空间
双亲委派模型:
三层类加载器:启动类加载器, 扩展类加载器, 应用程序类加载器
双亲委派模型:
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载 器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
破坏双亲委派模型
第一次“被破坏:引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的 protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。
第二次“被破坏”是由这个模型自身的缺陷导致的,有基础类型又要调用回用户的代码,引入了一个不太优雅的设计:线程上下文类加载器 (Thread Context ClassLoader)。
的第三次“被破坏”是由于用户对程序动态性的追求而导致的。
Java模块化系统:
够实现模块化的关键目标——可配置的封装隔离机制。
JDK 9的模块不仅仅像之前的JAR包那样只是 简单地充当代码的容器,除了代码外,Java的模块定义还包含以下内容:1依赖其他模块的列表。 ·2导出的包列表,即其他模块可以使用的列表。 ·3开放的包列表,即其他模块可反射访问模块的列表。 ·4使用的服务列表。 ·5提供服务的实现列表。
在JDK 9以后,如果启用了模块化进行封装,模块就可以声明对其他模块 的显式依赖,这样Java虚拟机就能够在启动时验证应用程序开发阶段设定好的依赖关系在运行期是否 完备,如有缺失那就直接启动失败,从而避免了很大一部分[1]由于类型依赖而引发的运行时异常。
模块的兼容性
简单来说,就是某个类库到底是模块还是传统的JAR包,只取决于它存放在哪种路径上。只要是放在类路径上的JAR文件,无论其中是否包含模块化信息(是否包含了module-info.class文件),它都会被当作传统的JAR包来对待;相应地,只要放在模块路径上的JAR文件,即使没有使用JMOD后缀,甚至说其中并不包含module-info.class文 件,它也仍然会被当作一个模块来对待。
3条规则保证了即使Java应用依然使用传统的类路径:
JAR文件在类路径的访问规则:所有类路径下的JAR文件及其他资源文件,都被视为自动打包在一个匿名模块(Unnamed Module)里,这个匿名模块几乎是没有任何隔离的,它可以看到和使用类路 上所有的包、JDK系统模块中所有的导出包,以及模块路径上所有模块中导出的包。
·模块在模块路径的访问规则:模块路径下的具名模块(Named Module)只能访问到它依赖定义中列明依赖的模块和包,匿名模块里所有的内容对具名模块来说都是不可见的,即具名模块看不见传统JAR包的内容。
·JAR文件在模块路径的访问规则:如果把一个传统的、不包含模块定义的JAR文件放置到模块路 径中,它就会变成一个自动模块(Automatic Module)。尽管不包含module-info.class,但自动模块将默认依赖于整个模块路径中的所有模块,因此可以访问到所有模块导出的包,自动模块也默认导出自己所有的包。
模块化下的类加载器:
模块化下的类加载器仍然发生了一些应该被 注意到变动,主要包括以下几个方面。
首先,是扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代。
其次,平台类加载器和应用程序类加载器都不再派生自java.net.URLClassLoader,如果有程序直接 依赖了这种继承关系,或者依赖了URLClassLoader类的特定方法,那代码很可能会在JDK 9及更高版 本的JDK中崩溃。
当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能 够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器 完成加载,也许这可以算是对双亲委派的第四次破坏
虚拟机字节码执行引擎
执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处 理过程是字节码解析执行的等效过程,输出的是执行结果。
运行时栈帧结构:
栈帧”(Stack Frame)则是用于支持虚拟机进行方法 调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。
局部变量表:
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,一个变量槽可以存放一个 32位以内的数据类型,Java中占用不超过32位存储空间的数据类型有boolean、byte、char、short、int、 float、reference和returnAddress这8种类型。
当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程, 即实参到形参的传递。
为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的。
操作数栈:
操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项 之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占 的栈容量为1,64位数据类型所占的栈容量为2。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。
另外在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在 大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。
动态连接:
道Class文件的常量池中存 有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。 另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。
方法返回地址:
当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法 返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用 者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种 退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)。
另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处 理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方 法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用 完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它 的上层调用者提供任何返回值的。
无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行。
方法调用:
法调用阶段唯一的任务就是确定被调用方法的版本
解析:调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法 的调用被称为解析(Resolution)。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本, Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final 修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引 用解析为该方法的直接引用。
解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号 引用全部转变为明确的直接引用,不必延迟到运行期再去完成。
分派:
1.静态分派:
所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载,静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
在很多情况下这个重载版本并不是“唯一”的,往往只能确定一个“相对更合适的”版本。这种模糊的结论在由0和1构成的计算机世界中算是个 比较稀罕的事件,产生这种模糊结论的主要原因是字面量天生的模糊性,它不需要定义,所以字面量 就没有显式的静态类型,它的静态类型只能通过语言、语法的规则去理解和推断。
2.动态分派:
我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
3.单分派与多分派
方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对 目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
如今Java语言是一门静态多分派、动态单分派的语言。
4.虚拟机动态分派的实现
动态分派的方法版本选择过程需要运行时在接收者类型的方法元数据中搜索合适的目标方法。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方 法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。
为了程序实现方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序 号,这样当类型变换时,仅需要变更查找的虚方法表,就可以从不同的虚方法表中按索引转换出所需 的入口地址。
虚方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把 该类的虚方法表也一同初始化完毕。